You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@wicket.apache.org by mg...@apache.org on 2012/03/12 11:23:50 UTC

[6/21] WICKET-4439 Move classes around so that there are no two packages with the same name in different modules

http://git-wip-us.apache.org/repos/asf/wicket/blob/1257c03d/wicket-core/src/main/java/org/apache/wicket/core/request/handler/PageProvider.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/handler/PageProvider.java b/wicket-core/src/main/java/org/apache/wicket/core/request/handler/PageProvider.java
new file mode 100644
index 0000000..f239c4d
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/handler/PageProvider.java
@@ -0,0 +1,412 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.wicket.core.request.handler;
+
+import org.apache.wicket.Application;
+import org.apache.wicket.core.request.mapper.IPageSource;
+import org.apache.wicket.core.request.mapper.StalePageException;
+import org.apache.wicket.page.IPageManager;
+import org.apache.wicket.pageStore.IPageStore;
+import org.apache.wicket.protocol.http.PageExpiredException;
+import org.apache.wicket.request.IRequestHandler;
+import org.apache.wicket.request.IRequestMapper;
+import org.apache.wicket.request.component.IRequestablePage;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+import org.apache.wicket.util.lang.Args;
+
+/**
+ * Provides page instance for request handlers. Each of the constructors has just enough information
+ * to get existing or create new page instance. Requesting or creating page instance is deferred
+ * until {@link #getPageInstance()} is called.
+ * <p>
+ * Purpose of this class is to reduce complexity of both {@link IRequestMapper}s and
+ * {@link IRequestHandler}s. {@link IRequestMapper} examines the URL, gathers all relevant
+ * information about the page in the URL (combination of page id, page class, page parameters and
+ * render count), creates {@link PageProvider} object and creates a {@link IRequestHandler} instance
+ * that can use the {@link PageProvider} to access the page.
+ * <p>
+ * Apart from simplifying {@link IRequestMapper}s and {@link IRequestHandler}s {@link PageProvider}
+ * also helps performance because creating or obtaining page from {@link IPageManager} is delayed
+ * until the {@link IRequestHandler} actually requires the page.
+ *
+ * @author Matej Knopp
+ */
+public class PageProvider implements IPageProvider
+{
+	private final Integer renderCount;
+
+	private final Integer pageId;
+
+	private IPageSource pageSource;
+
+	private IRequestablePage pageInstance;
+	private boolean pageInstanceIsFresh;
+
+	private Class<? extends IRequestablePage> pageClass;
+
+	private PageParameters pageParameters;
+
+	/**
+	 * Creates a new page provider object. Upon calling of {@link #getPageInstance()} this provider
+	 * will return page instance with specified id.
+	 *
+	 * @param pageId
+	 * @param renderCount
+	 *            optional argument
+	 */
+	public PageProvider(final int pageId, final Integer renderCount)
+	{
+		this.pageId = pageId;
+		this.renderCount = renderCount;
+	}
+
+	/**
+	 * Creates a new page provider object. Upon calling of {@link #getPageInstance()} this provider
+	 * will return page instance with specified id if it exists and it's class matches pageClass. If
+	 * none of these is true new page instance will be created.
+	 *
+	 * @param pageId
+	 * @param pageClass
+	 * @param renderCount
+	 *            optional argument
+	 */
+	public PageProvider(final int pageId, final Class<? extends IRequestablePage> pageClass,
+		Integer renderCount)
+	{
+		this(pageId, pageClass, new PageParameters(), renderCount);
+	}
+
+	/**
+	 * Creates a new page provider object. Upon calling of {@link #getPageInstance()} this provider
+	 * will return page instance with specified id if it exists and it's class matches pageClass. If
+	 * none of these is true new page instance will be created.
+	 *
+	 * @param pageId
+	 * @param pageClass
+	 * @param pageParameters
+	 * @param renderCount
+	 *            optional argument
+	 */
+	public PageProvider(final int pageId, final Class<? extends IRequestablePage> pageClass,
+		final PageParameters pageParameters, final Integer renderCount)
+	{
+		this.pageId = pageId;
+		setPageClass(pageClass);
+		setPageParameters(pageParameters);
+		this.renderCount = renderCount;
+	}
+
+	/**
+	 * Creates a new page provider object. Upon calling of {@link #getPageInstance()} this provider
+	 * will return new instance of page with specified class.
+	 *
+	 * @param pageClass
+	 * @param pageParameters
+	 */
+	public PageProvider(final Class<? extends IRequestablePage> pageClass,
+		final PageParameters pageParameters)
+	{
+		setPageClass(pageClass);
+		if (pageParameters != null)
+		{
+			setPageParameters(pageParameters);
+		}
+		pageId = null;
+		renderCount = null;
+	}
+
+	/**
+	 * Creates a new page provider object. Upon calling of {@link #getPageInstance()} this provider
+	 * will return new instance of page with specified class.
+	 *
+	 * @param pageClass
+	 */
+	public PageProvider(Class<? extends IRequestablePage> pageClass)
+	{
+		this(pageClass, null);
+	}
+
+	/**
+	 * Creates a new page provider object. Upon calling of {@link #getPageInstance()} this provider
+	 * will return the given page instance.
+	 *
+	 * @param page
+	 */
+	public PageProvider(IRequestablePage page)
+	{
+		Args.notNull(page, "page");
+
+		pageInstance = page;
+		pageId = page.getPageId();
+		renderCount = page.getRenderCount();
+	}
+
+	/**
+	 * @see IPageProvider#getPageInstance()
+	 */
+	@Override
+	public IRequestablePage getPageInstance()
+	{
+		if (pageInstance == null)
+		{
+			resolvePageInstance(pageId, pageClass, pageParameters, renderCount);
+
+			if (pageInstance == null)
+			{
+				throw new PageExpiredException("Page with id '" + pageId + "' has expired.");
+			}
+		}
+		return pageInstance;
+	}
+
+	/**
+	 * @see IPageProvider#getPageParameters()
+	 */
+	@Override
+	public PageParameters getPageParameters()
+	{
+		if (pageParameters != null)
+		{
+			return pageParameters;
+		}
+		else if (isNewPageInstance() == false)
+		{
+			return pageInstance.getPageParameters();
+		}
+		else
+		{
+			return null;
+		}
+	}
+
+	/**
+	 * The page instance is new only if there is no cached instance or the data stores doesn't have
+	 * a page with that id with the same {@linkplain #pageClass}.
+	 *
+	 * @see IPageProvider#isNewPageInstance()
+	 */
+	@Override
+	public boolean isNewPageInstance()
+	{
+		boolean isNew = pageInstance == null;
+		if (isNew && pageId != null)
+		{
+			IRequestablePage storedPageInstance = getStoredPage(pageId);
+			if (storedPageInstance != null)
+			{
+				pageInstance = storedPageInstance;
+				isNew = false;
+			}
+		}
+
+		return isNew;
+	}
+
+	/**
+	 * @see IPageProvider#getPageClass()
+	 */
+	@Override
+	public Class<? extends IRequestablePage> getPageClass()
+	{
+		if (pageClass != null)
+		{
+			return pageClass;
+		}
+		else
+		{
+			return getPageInstance().getClass();
+		}
+	}
+
+	protected IPageSource getPageSource()
+	{
+		if (pageSource != null)
+		{
+			return pageSource;
+		}
+		if (Application.exists())
+		{
+			return Application.get().getMapperContext();
+		}
+		else
+		{
+			throw new IllegalStateException(
+				"No application is bound to current thread. Call setPageSource() to manually assign pageSource to this provider.");
+		}
+	}
+
+	private void resolvePageInstance(Integer pageId, Class<? extends IRequestablePage> pageClass,
+		PageParameters pageParameters, Integer renderCount)
+	{
+		IRequestablePage page = null;
+
+		boolean freshCreated = false;
+
+		if (pageId != null)
+		{
+			page = getStoredPage(pageId);
+		}
+
+		if (page == null)
+		{
+			if (pageClass != null)
+			{
+				page = getPageSource().newPageInstance(pageClass, pageParameters);
+				freshCreated = true;
+			}
+		}
+
+		if (page != null && !freshCreated)
+		{
+			if (renderCount != null && page.getRenderCount() != renderCount)
+			{
+				throw new StalePageException(page);
+			}
+		}
+
+		pageInstanceIsFresh = freshCreated;
+		pageInstance = page;
+	}
+
+	/**
+	 * Looks up a page by id from the {@link IPageStore}. <br/>
+	 * If {@linkplain #pageClass} is specified then compares it against the stored instance class
+	 * and returns the found instance only if they match.
+	 *
+	 * @param pageId
+	 *            the id of the page to look for.
+	 * @return the found page instance by id.
+	 */
+	private IRequestablePage getStoredPage(final int pageId)
+	{
+		IRequestablePage storedPageInstance = getPageSource().getPageInstance(pageId);
+		if (storedPageInstance != null &&
+			(pageClass == null || pageClass.equals(storedPageInstance.getClass())))
+		{
+			pageInstance = storedPageInstance;
+			pageInstanceIsFresh = false;
+			if (pageInstance != null)
+			{
+				if (renderCount != null && pageInstance.getRenderCount() != renderCount)
+				{
+					throw new StalePageException(pageInstance);
+				}
+			}
+		}
+		return storedPageInstance;
+	}
+
+	/**
+	 * Detaches the page if it has been loaded (that means either
+	 * {@link #PageProvider(IRequestablePage)} constructor has been used or
+	 * {@link #getPageInstance()} has been called).
+	 */
+	@Override
+	public void detach()
+	{
+		if (pageInstance != null)
+		{
+			pageInstance.detach();
+		}
+	}
+
+	/**
+	 * If the {@link PageProvider} is used outside request thread (thread that does not have
+	 * application instance assigned) it is necessary to specify a {@link IPageSource} instance so
+	 * that {@link PageProvider} knows how to get a page instance.
+	 *
+	 * @param pageSource
+	 */
+	public void setPageSource(IPageSource pageSource)
+	{
+		this.pageSource = pageSource;
+	}
+
+	/**
+	 *
+	 * @param pageClass
+	 */
+	private void setPageClass(Class<? extends IRequestablePage> pageClass)
+	{
+		Args.notNull(pageClass, "pageClass");
+
+		this.pageClass = pageClass;
+	}
+
+	/**
+	 *
+	 * @param pageParameters
+	 */
+	protected void setPageParameters(PageParameters pageParameters)
+	{
+		this.pageParameters = pageParameters;
+	}
+
+	/**
+	 *
+	 * @return page id
+	 */
+	@Override
+	public Integer getPageId()
+	{
+		return pageId;
+	}
+
+	@Override
+	public Integer getRenderCount()
+	{
+		return renderCount;
+	}
+
+	/**
+	 * Checks whether or not the provider has a page instance. This page instance might have been
+	 * passed to this page provider directly or it may have been instantiated or retrieved from the
+	 * page store.
+	 *
+	 * @return {@code true} iff page instance has been created or retrieved
+	 */
+	@Override
+	public final boolean hasPageInstance()
+	{
+		if (pageInstance == null && pageId != null)
+		{
+			// attempt to load a stored page instance from the page store
+			getStoredPage(pageId);
+		}
+		return pageInstance != null;
+	}
+
+	/**
+	 * Returns whether or not the page instance held by this provider has been instantiated by the
+	 * provider.
+	 *
+	 * @throws IllegalStateException
+	 *             if this method is called and the provider does not yet have a page instance, ie
+	 *             if {@link #getPageInstance()} has never been called on this provider
+	 * @return {@code true} iff the page instance held by this provider was instantiated by the
+	 *         provider
+	 */
+	@Override
+	public final boolean isPageInstanceFresh()
+	{
+		if (!hasPageInstance())
+		{
+			throw new IllegalStateException("Page instance not yet resolved");
+		}
+		return pageInstanceIsFresh;
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/1257c03d/wicket-core/src/main/java/org/apache/wicket/core/request/handler/RenderPageRequestHandler.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/handler/RenderPageRequestHandler.java b/wicket-core/src/main/java/org/apache/wicket/core/request/handler/RenderPageRequestHandler.java
new file mode 100644
index 0000000..94dec22
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/handler/RenderPageRequestHandler.java
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.wicket.core.request.handler;
+
+import org.apache.wicket.Application;
+import org.apache.wicket.core.request.handler.logger.PageLogData;
+import org.apache.wicket.request.ILoggableRequestHandler;
+import org.apache.wicket.request.IRequestCycle;
+import org.apache.wicket.request.IRequestHandler;
+import org.apache.wicket.request.component.IRequestablePage;
+import org.apache.wicket.request.cycle.RequestCycle;
+import org.apache.wicket.request.handler.render.PageRenderer;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+import org.apache.wicket.util.lang.Args;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@link IRequestHandler} that renders page instance. Depending on the <code>redirectPolicy</code>
+ * flag and current request strategy the handler either just renders the page to the response, or
+ * redirects to render the page. <code>REDIRECT_TO_BUFFER</code> strategy is also supported.
+ * <p>
+ *
+ * @author Matej Knopp
+ */
+public class RenderPageRequestHandler
+	implements
+		IPageRequestHandler,
+		IPageClassRequestHandler,
+		ILoggableRequestHandler
+{
+	private static final Logger logger = LoggerFactory.getLogger(RenderPageRequestHandler.class);
+
+	private final IPageProvider pageProvider;
+
+	private final RedirectPolicy redirectPolicy;
+
+	private PageLogData logData;
+
+	/**
+	 * Determines whether Wicket does a redirect when rendering a page
+	 *
+	 * @author Matej Knopp
+	 */
+	public enum RedirectPolicy {
+		/**
+		 * Always redirect if current request URL is different than page URL.
+		 */
+		ALWAYS_REDIRECT,
+
+		/**
+		 * Never redirect - always render the page to current response.
+		 */
+		NEVER_REDIRECT,
+
+		/**
+		 * Redirect if necessary. The redirect will happen when all of the following conditions are
+		 * met:
+		 * <ul>
+		 * <li>current request URL is different than page URL
+		 * <li>page is not stateless or (page is stateless and session is not temporary)
+		 * <li>render strategy is either REDIRECT_TO_BUFFER or REDIRECT_TO_RENDER
+		 * </ul>
+		 */
+		AUTO_REDIRECT
+	}
+
+	/**
+	 * Construct. Renders the page with a redirect if necessary.
+	 *
+	 * @param pageProvider
+	 */
+	public RenderPageRequestHandler(IPageProvider pageProvider)
+	{
+		this(pageProvider, RedirectPolicy.AUTO_REDIRECT);
+	}
+
+	/**
+	 * Construct.
+	 *
+	 * @param pageProvider
+	 * @param redirectPolicy
+	 */
+	public RenderPageRequestHandler(IPageProvider pageProvider, RedirectPolicy redirectPolicy)
+	{
+		Args.notNull(pageProvider, "pageProvider");
+		Args.notNull(redirectPolicy, "redirectPolicy");
+
+		this.redirectPolicy = redirectPolicy;
+		this.pageProvider = pageProvider;
+	}
+
+	/**
+	 * @return page provider
+	 */
+	public IPageProvider getPageProvider()
+	{
+		return pageProvider;
+	}
+
+	/**
+	 * @return redirect policy
+	 */
+	public RedirectPolicy getRedirectPolicy()
+	{
+		return redirectPolicy;
+	}
+
+	@Override
+	public Class<? extends IRequestablePage> getPageClass()
+	{
+		return pageProvider.getPageClass();
+	}
+
+	@Override
+	public Integer getPageId()
+	{
+		return pageProvider.getPageId();
+	}
+
+	@Override
+	public PageParameters getPageParameters()
+	{
+		return pageProvider.getPageParameters();
+	}
+
+	@Override
+	public void detach(IRequestCycle requestCycle)
+	{
+		if (logData == null)
+			logData = new PageLogData(pageProvider);
+		pageProvider.detach();
+	}
+
+	@Override
+	public PageLogData getLogData()
+	{
+		return logData;
+	}
+
+	@Override
+	public IRequestablePage getPage()
+	{
+		return pageProvider.getPageInstance();
+	}
+
+	@Override
+	public void respond(IRequestCycle requestCycle)
+	{
+		PageRenderer renderer = Application.get().getPageRendererProvider().get(this);
+		renderer.respond((RequestCycle)requestCycle);
+	}
+
+	@Override
+	public final boolean isPageInstanceCreated()
+	{
+		return pageProvider.hasPageInstance();
+	}
+
+	@Override
+	public final Integer getRenderCount()
+	{
+		return pageProvider.getRenderCount();
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/1257c03d/wicket-core/src/main/java/org/apache/wicket/core/request/handler/logger/ListenerInterfaceLogData.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/handler/logger/ListenerInterfaceLogData.java b/wicket-core/src/main/java/org/apache/wicket/core/request/handler/logger/ListenerInterfaceLogData.java
new file mode 100644
index 0000000..8e73163
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/handler/logger/ListenerInterfaceLogData.java
@@ -0,0 +1,243 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.wicket.core.request.handler.logger;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.RequestListenerInterface;
+import org.apache.wicket.behavior.Behavior;
+import org.apache.wicket.core.request.handler.IPageAndComponentProvider;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.markup.html.form.IFormSubmitListener;
+import org.apache.wicket.markup.html.form.IFormSubmitter;
+import org.apache.wicket.request.component.IRequestableComponent;
+
+/**
+ * Contains logging data for component/listener-interface request handlers.
+ *
+ * @author Emond Papegaaij
+ */
+public class ListenerInterfaceLogData extends PageLogData
+{
+	private static final long serialVersionUID = 1L;
+
+	private final Class<? extends IRequestableComponent> componentClass;
+	private final String componentPath;
+	private final Integer behaviorIndex;
+	private Class<? extends Behavior> behaviorClass;
+	private final String interfaceName;
+	private final String interfaceMethod;
+	private Class<? extends IRequestableComponent> submittingComponentClass;
+	private String submittingComponentPath;
+
+	/**
+	 * Construct.
+	 *
+	 * @param pageAndComponentProvider
+	 * @param listenerInterface
+	 * @param behaviorIndex
+	 */
+	public ListenerInterfaceLogData(IPageAndComponentProvider pageAndComponentProvider,
+		RequestListenerInterface listenerInterface, Integer behaviorIndex)
+	{
+		super(pageAndComponentProvider);
+		componentClass = tryToGetComponentClass(pageAndComponentProvider);
+		componentPath = tryToGetComponentPath(pageAndComponentProvider);
+		this.behaviorIndex = behaviorIndex;
+		if (behaviorIndex != null && componentClass != null)
+		{
+			try
+			{
+				behaviorClass = pageAndComponentProvider.getComponent()
+					.getBehaviorById(behaviorIndex)
+					.getClass();
+			}
+			catch (Exception ignore)
+			{
+				behaviorClass = null;
+			}
+		}
+		else
+		{
+			behaviorClass = null;
+		}
+		interfaceName = listenerInterface.getName();
+		interfaceMethod = listenerInterface.getMethod().getName();
+		if (listenerInterface.getListenerInterfaceClass().equals(IFormSubmitListener.class))
+		{
+			final Component formSubmitter = tryToGetFormSubmittingComponent(pageAndComponentProvider);
+			if (formSubmitter != null)
+			{
+				submittingComponentClass = formSubmitter.getClass();
+				submittingComponentPath = formSubmitter.getPageRelativePath();
+			}
+		}
+	}
+
+	private static Class<? extends IRequestableComponent> tryToGetComponentClass(
+		IPageAndComponentProvider pageAndComponentProvider)
+	{
+		try
+		{
+			return pageAndComponentProvider.getComponent().getClass();
+		}
+		catch (Exception e)
+		{
+			// getComponent might fail if the page does not exist (ie session timeout)
+			return null;
+		}
+	}
+
+
+	private static String tryToGetComponentPath(IPageAndComponentProvider pageAndComponentProvider)
+	{
+		try
+		{
+			return pageAndComponentProvider.getComponentPath();
+		}
+		catch (Exception e)
+		{
+			// getComponentPath might fail if the page does not exist (ie session timeout)
+			return null;
+		}
+	}
+
+	private static Component tryToGetFormSubmittingComponent(
+		IPageAndComponentProvider pageAndComponentProvider)
+	{
+		try
+		{
+			final IRequestableComponent component = pageAndComponentProvider.getComponent();
+			if (component instanceof Form)
+			{
+				final IFormSubmitter submitter = ((Form<?>)component).findSubmittingButton();
+				return submitter instanceof Component ? (Component)submitter : null;
+			}
+			return null;
+		}
+		catch (Exception e)
+		{
+			// getComponent might fail if the page does not exist (ie session timeout)
+			return null;
+		}
+	}
+
+	/**
+	 * @return componentClass
+	 */
+	public final Class<? extends IRequestableComponent> getComponentClass()
+	{
+		return componentClass;
+	}
+
+	/**
+	 * @return componentPath
+	 */
+	public final String getComponentPath()
+	{
+		return componentPath;
+	}
+
+	/**
+	 * @return behaviorIndex
+	 */
+	public final Integer getBehaviorIndex()
+	{
+		return behaviorIndex;
+	}
+
+	/**
+	 * @return behaviorClass
+	 */
+	public final Class<? extends Behavior> getBehaviorClass()
+	{
+		return behaviorClass;
+	}
+
+	/**
+	 * @return interfaceName
+	 */
+	public final String getInterfaceName()
+	{
+		return interfaceName;
+	}
+
+	/**
+	 * @return interfaceMethod
+	 */
+	public final String getInterfaceMethod()
+	{
+		return interfaceMethod;
+	}
+
+	/**
+	 * @return submittingComponentClass
+	 */
+	public Class<? extends IRequestableComponent> getSubmittingComponentClass()
+	{
+		return submittingComponentClass;
+	}
+
+	/**
+	 * @return submittingComponentPath
+	 */
+	public String getSubmittingComponentPath()
+	{
+		return submittingComponentPath;
+	}
+
+	@Override
+	public String toString()
+	{
+		StringBuilder sb = new StringBuilder(super.toString());
+		sb.setCharAt(sb.length() - 1, ',');
+		if (getComponentClass() != null)
+		{
+			sb.append("componentClass=");
+			sb.append(getComponentClass().getName());
+			sb.append(',');
+		}
+		if (getComponentPath() != null)
+		{
+			sb.append("componentPath=");
+			sb.append(getComponentPath());
+			sb.append(',');
+		}
+		sb.append("behaviorIndex=");
+		sb.append(getBehaviorIndex());
+		if (getBehaviorClass() != null)
+		{
+			sb.append(",behaviorClass=");
+			sb.append(getBehaviorClass().getName());
+		}
+		sb.append(",interfaceName=");
+		sb.append(getInterfaceName());
+		sb.append(",interfaceMethod=");
+		sb.append(getInterfaceMethod());
+		if (getSubmittingComponentClass() != null)
+		{
+			sb.append(",submittingComponentClass=");
+			sb.append(getSubmittingComponentClass().getName());
+		}
+		if (getSubmittingComponentPath() != null)
+		{
+			sb.append(",submittingComponentPath=");
+			sb.append(getSubmittingComponentPath());
+		}
+		sb.append("}");
+		return sb.toString();
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/1257c03d/wicket-core/src/main/java/org/apache/wicket/core/request/handler/logger/PageLogData.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/handler/logger/PageLogData.java b/wicket-core/src/main/java/org/apache/wicket/core/request/handler/logger/PageLogData.java
new file mode 100644
index 0000000..fd3b53f
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/handler/logger/PageLogData.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.wicket.core.request.handler.logger;
+
+import org.apache.wicket.Page;
+import org.apache.wicket.core.request.handler.IPageClassRequestHandler;
+import org.apache.wicket.core.request.handler.IPageProvider;
+import org.apache.wicket.core.request.handler.IPageRequestHandler;
+import org.apache.wicket.request.ILogData;
+import org.apache.wicket.request.component.IRequestablePage;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+
+/**
+ * Contains logging data for request handlers that are related to pages; most likely
+ * {@link IPageRequestHandler} or {@link IPageClassRequestHandler}.
+ *
+ * @author Emond Papegaaij
+ */
+public class PageLogData implements ILogData
+{
+	private static final long serialVersionUID = 1L;
+
+	private final Class<? extends IRequestablePage> pageClass;
+	private final Integer pageId;
+	private final PageParameters pageParameters;
+	private final Integer renderCount;
+
+	/**
+	 * Construct.
+	 *
+	 * @param pageProvider
+	 */
+	public PageLogData(IPageProvider pageProvider)
+	{
+		pageClass = tryToGetPageClass(pageProvider);
+		pageId = pageProvider.getPageId();
+		pageParameters = pageProvider.getPageParameters();
+		renderCount = pageProvider.getRenderCount();
+	}
+
+	private static Class<? extends IRequestablePage> tryToGetPageClass(IPageProvider pageProvider)
+	{
+		try
+		{
+			return pageProvider.getPageClass();
+		}
+		catch (Exception e)
+		{
+			// getPageClass might fail if the page does not exist (ie session timeout)
+			return null;
+		}
+	}
+
+	/**
+	 * Construct.
+	 *
+	 * @param page
+	 */
+	public PageLogData(Page page)
+	{
+		pageClass = page.getPageClass();
+		pageId = page.getPageId();
+		pageParameters = page.getPageParameters();
+		renderCount = page.getRenderCount();
+	}
+
+	/**
+	 * @return pageClass
+	 */
+	public final Class<? extends IRequestablePage> getPageClass()
+	{
+		return pageClass;
+	}
+
+	/**
+	 * @return pageId
+	 */
+	public final Integer getPageId()
+	{
+		return pageId;
+	}
+
+	/**
+	 * @return pageParameters
+	 */
+	public final PageParameters getPageParameters()
+	{
+		return pageParameters;
+	}
+
+	/**
+	 * @return renderCount
+	 */
+	public final Integer getRenderCount()
+	{
+		return renderCount;
+	}
+
+	@Override
+	public String toString()
+	{
+		StringBuilder sb = new StringBuilder("{");
+		if (pageClass != null)
+		{
+			sb.append("pageClass=");
+			sb.append(getPageClass().getName());
+			sb.append(',');
+		}
+		sb.append("pageId=");
+		sb.append(getPageId());
+		sb.append(",pageParameters={");
+		sb.append(getPageParameters());
+		sb.append("},renderCount=");
+		sb.append(getRenderCount());
+		sb.append("}");
+		return sb.toString();
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/1257c03d/wicket-core/src/main/java/org/apache/wicket/core/request/handler/logger/ResourceReferenceLogData.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/handler/logger/ResourceReferenceLogData.java b/wicket-core/src/main/java/org/apache/wicket/core/request/handler/logger/ResourceReferenceLogData.java
new file mode 100644
index 0000000..f4c634b
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/handler/logger/ResourceReferenceLogData.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.wicket.core.request.handler.logger;
+
+import org.apache.wicket.request.handler.resource.ResourceLogData;
+import org.apache.wicket.request.handler.resource.ResourceReferenceRequestHandler;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+import org.apache.wicket.request.resource.ResourceReference;
+
+/**
+ * Contains logging data for resource reference requests.
+ *
+ * @author Emond Papegaaij
+ */
+public class ResourceReferenceLogData extends ResourceLogData
+{
+	private static final long serialVersionUID = 1L;
+
+	private final Class<? extends ResourceReference> resourceReferenceClass;
+	private final Class<?> scope;
+	private final PageParameters pageParameters;
+
+	/**
+	 * Construct.
+	 *
+	 * @param refHandler
+	 */
+	public ResourceReferenceLogData(ResourceReferenceRequestHandler refHandler)
+	{
+		super(refHandler.getResourceReference().getName(), refHandler.getLocale(),
+			refHandler.getStyle(), refHandler.getVariation());
+		resourceReferenceClass = refHandler.getResourceReference().getClass();
+		scope = refHandler.getResourceReference().getScope();
+		pageParameters = refHandler.getPageParameters();
+	}
+
+	/**
+	 * @return resourceReferenceClass
+	 */
+	public final Class<? extends ResourceReference> getResourceReferenceClass()
+	{
+		return resourceReferenceClass;
+	}
+
+	/**
+	 * @return scope
+	 */
+	public final Class<?> getScope()
+	{
+		return scope;
+	}
+
+	/***
+	 * @return pageParameters
+	 */
+	public final PageParameters getPageParameters()
+	{
+		return pageParameters;
+	}
+
+	@Override
+	public String toString()
+	{
+		StringBuilder sb = new StringBuilder("{");
+		fillToString(sb);
+		sb.append(",resourceReferenceClass=");
+		sb.append(getResourceReferenceClass().getName());
+		sb.append(",scope=");
+		sb.append(getScope() == null ? "null" : getScope().getName());
+		sb.append(",pageParameters={");
+		sb.append(getPageParameters());
+		sb.append("}}");
+		return sb.toString();
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/1257c03d/wicket-core/src/main/java/org/apache/wicket/core/request/handler/logger/ResourceStreamLogData.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/handler/logger/ResourceStreamLogData.java b/wicket-core/src/main/java/org/apache/wicket/core/request/handler/logger/ResourceStreamLogData.java
new file mode 100644
index 0000000..088fec8
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/handler/logger/ResourceStreamLogData.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.wicket.core.request.handler.logger;
+
+import org.apache.wicket.request.handler.resource.ResourceLogData;
+import org.apache.wicket.request.handler.resource.ResourceStreamRequestHandler;
+import org.apache.wicket.request.resource.ContentDisposition;
+import org.apache.wicket.util.resource.IResourceStream;
+
+/**
+ * Contains logging data for resource stream requests, handled by
+ * {@link ResourceStreamRequestHandler}.
+ *
+ * @author Emond Papegaaij
+ */
+public class ResourceStreamLogData extends ResourceLogData
+{
+	private static final long serialVersionUID = 1L;
+
+	private final Class<? extends IResourceStream> resourceStreamClass;
+	private final ContentDisposition contentDisposition;
+	private final String contentType;
+
+	/**
+	 * Construct.
+	 *
+	 * @param streamHandler
+	 */
+	public ResourceStreamLogData(ResourceStreamRequestHandler streamHandler)
+	{
+		super(streamHandler.getFileName(), null, null, null);
+		contentDisposition = streamHandler.getContentDisposition();
+		resourceStreamClass = null;
+		contentType = null;
+	}
+
+	/**
+	 * Construct.
+	 *
+	 * @param streamHandler
+	 * @param stream
+	 */
+	public ResourceStreamLogData(ResourceStreamRequestHandler streamHandler, IResourceStream stream)
+	{
+		super(streamHandler.getFileName(), stream.getLocale(), stream.getStyle(),
+			stream.getVariation());
+		contentDisposition = streamHandler.getContentDisposition();
+		resourceStreamClass = stream.getClass();
+		contentType = stream.getContentType();
+	}
+
+	/**
+	 * Returns the class of the resource stream.
+	 *
+	 * @return The class of the resource stream.
+	 */
+	public final Class<? extends IResourceStream> getResourceStreamClass()
+	{
+		return resourceStreamClass;
+	}
+
+	/**
+	 * @return contentDisposition.
+	 */
+	public final ContentDisposition getContentDisposition()
+	{
+		return contentDisposition;
+	}
+
+	/**
+	 * @return contentType
+	 */
+	public final String getContentType()
+	{
+		return contentType;
+	}
+
+	@Override
+	public String toString()
+	{
+		StringBuilder sb = new StringBuilder("{");
+		fillToString(sb);
+		sb.append(",contentDisposition=");
+		sb.append(getContentDisposition());
+		if (getResourceStreamClass() != null)
+		{
+			sb.append(",resourceStreamClass=");
+			sb.append(getResourceStreamClass().getName());
+			sb.append(",contentType=");
+			sb.append(getContentType());
+		}
+		sb.append("}");
+		return sb.toString();
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/1257c03d/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/AbstractBookmarkableMapper.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/AbstractBookmarkableMapper.java b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/AbstractBookmarkableMapper.java
new file mode 100644
index 0000000..0f65fb0
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/AbstractBookmarkableMapper.java
@@ -0,0 +1,414 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.wicket.core.request.mapper;
+
+import org.apache.wicket.RequestListenerInterface;
+import org.apache.wicket.core.request.handler.BookmarkableListenerInterfaceRequestHandler;
+import org.apache.wicket.core.request.handler.BookmarkablePageRequestHandler;
+import org.apache.wicket.core.request.handler.ListenerInterfaceRequestHandler;
+import org.apache.wicket.core.request.handler.PageAndComponentProvider;
+import org.apache.wicket.core.request.handler.PageProvider;
+import org.apache.wicket.core.request.handler.RenderPageRequestHandler;
+import org.apache.wicket.request.IRequestHandler;
+import org.apache.wicket.request.IRequestHandlerDelegate;
+import org.apache.wicket.request.IRequestMapper;
+import org.apache.wicket.request.Request;
+import org.apache.wicket.request.Url;
+import org.apache.wicket.request.component.IRequestablePage;
+import org.apache.wicket.request.http.WebRequest;
+import org.apache.wicket.request.mapper.info.ComponentInfo;
+import org.apache.wicket.request.mapper.info.PageComponentInfo;
+import org.apache.wicket.request.mapper.info.PageInfo;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+import org.apache.wicket.util.lang.Args;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Abstract encoder for Bookmarkable, Hybrid and BookmarkableListenerInterface URLs.
+ *
+ * @author Matej Knopp
+ */
+public abstract class AbstractBookmarkableMapper extends AbstractComponentMapper
+{
+	private static Logger logger = LoggerFactory.getLogger(AbstractBookmarkableMapper.class);
+
+	/**
+	 * Represents information stored in URL.
+	 *
+	 * @author Matej Knopp
+	 */
+	protected static final class UrlInfo
+	{
+		private final PageComponentInfo pageComponentInfo;
+		private final PageParameters pageParameters;
+		private final Class<? extends IRequestablePage> pageClass;
+
+		/**
+		 * Construct.
+		 *
+		 * @param pageComponentInfo
+		 *            optional parameter providing the page instance and component information
+		 * @param pageClass
+		 *            mandatory parameter
+		 * @param pageParameters
+		 *            optional parameter providing pageParameters
+		 */
+		public UrlInfo(PageComponentInfo pageComponentInfo,
+			Class<? extends IRequestablePage> pageClass, PageParameters pageParameters)
+		{
+			Args.notNull(pageClass, "pageClass");
+
+			this.pageComponentInfo = pageComponentInfo;
+			this.pageParameters = cleanPageParameters(pageParameters);
+
+			this.pageClass = pageClass;
+		}
+
+		/**
+		 * Cleans the original parameters from entries used by Wicket internals.
+		 *
+		 * @param originalParameters
+		 *            the current request's non-modified parameters
+		 * @return all parameters but Wicket internal ones
+		 */
+		private PageParameters cleanPageParameters(final PageParameters originalParameters)
+		{
+			PageParameters cleanParameters = null;
+			if (originalParameters != null)
+			{
+				cleanParameters = new PageParameters(originalParameters);
+
+				// WICKET-4038: Ajax related parameters are set by wicket-ajax.js when needed.
+				// They shouldn't be propagated to the next requests
+				cleanParameters.remove(WebRequest.PARAM_AJAX);
+				cleanParameters.remove(WebRequest.PARAM_AJAX_BASE_URL);
+				cleanParameters.remove(WebRequest.PARAM_AJAX_REQUEST_ANTI_CACHE);
+
+				if (cleanParameters.isEmpty())
+				{
+					cleanParameters = null;
+				}
+			}
+			return cleanParameters;
+		}
+
+		/**
+		 * @return PageComponentInfo instance or <code>null</code>
+		 */
+		public PageComponentInfo getPageComponentInfo()
+		{
+			return pageComponentInfo;
+		}
+
+		/**
+		 * @return page class
+		 */
+		public Class<? extends IRequestablePage> getPageClass()
+		{
+			return pageClass;
+		}
+
+		/**
+		 * @return PageParameters instance (never <code>null</code>)
+		 */
+		public PageParameters getPageParameters()
+		{
+			return pageParameters;
+		}
+	}
+
+	/**
+	 * Construct.
+	 */
+	public AbstractBookmarkableMapper()
+	{
+	}
+
+	/**
+	 * Parse the given request to an {@link UrlInfo} instance.
+	 *
+	 * @param request
+	 * @return UrlInfo instance or <code>null</code> if this encoder can not handle the request
+	 */
+	protected abstract UrlInfo parseRequest(Request request);
+
+	/**
+	 * Builds URL for the given {@link UrlInfo} instance. The URL this method produces must be
+	 * parseable by the {@link #parseRequest(Request)} method.
+	 *
+	 * @param info
+	 * @return Url result URL
+	 */
+	protected abstract Url buildUrl(UrlInfo info);
+
+	/**
+	 * Indicates whether hybrid {@link RenderPageRequestHandler} URL for page will be generated only
+	 * if page has been created with bookmarkable URL.
+	 * <p>
+	 * For generic bookmarkable encoders this method should return <code>true</code>. For explicit
+	 * (mounted) encoders this method should return <code>false</code>
+	 *
+	 * @return <code>true</code> if hybrid URL requires page created bookmarkable,
+	 *         <code>false</code> otherwise.
+	 */
+	protected abstract boolean pageMustHaveBeenCreatedBookmarkable();
+
+	/**
+	 * @see IRequestMapper#getCompatibilityScore(Request)
+	 */
+	@Override
+	public abstract int getCompatibilityScore(Request request);
+
+	/**
+	 * Creates a {@code IRequestHandler} that processes a bookmarkable request.
+	 *
+	 * @param pageClass
+	 * @param pageParameters
+	 * @return a {@code IRequestHandler} capable of processing the bookmarkable request.
+	 */
+	protected IRequestHandler processBookmarkable(Class<? extends IRequestablePage> pageClass,
+		PageParameters pageParameters)
+	{
+		PageProvider provider = new PageProvider(pageClass, pageParameters);
+		provider.setPageSource(getContext());
+		return new RenderPageRequestHandler(provider);
+	}
+
+	/**
+	 * Creates a {@code IRequestHandler} that processes a hybrid request. When the page identified
+	 * by {@code pageInfo} was not available, the request should be treated as a bookmarkable
+	 * request.
+	 *
+	 * @param pageInfo
+	 * @param pageClass
+	 * @param pageParameters
+	 * @param renderCount
+	 * @return a {@code IRequestHandler} capable of processing the hybrid request.
+	 */
+	protected IRequestHandler processHybrid(PageInfo pageInfo,
+		Class<? extends IRequestablePage> pageClass, PageParameters pageParameters,
+		Integer renderCount)
+	{
+		PageProvider provider = new PageProvider(pageInfo.getPageId(), pageClass, pageParameters,
+			renderCount);
+		provider.setPageSource(getContext());
+		return new RenderPageRequestHandler(provider);
+	}
+
+	/**
+	 * Creates a {@code IRequestHandler} that processes a listener request.
+	 *
+	 * @param pageComponentInfo
+	 * @param pageClass
+	 * @param pageParameters
+	 * @return a {@code IRequestHandler} that invokes the listener interface
+	 */
+	protected IRequestHandler processListener(PageComponentInfo pageComponentInfo,
+		Class<? extends IRequestablePage> pageClass, PageParameters pageParameters)
+	{
+		PageInfo pageInfo = pageComponentInfo.getPageInfo();
+		ComponentInfo componentInfo = pageComponentInfo.getComponentInfo();
+		Integer renderCount = null;
+		RequestListenerInterface listenerInterface = null;
+
+		if (componentInfo != null)
+		{
+			renderCount = componentInfo.getRenderCount();
+			listenerInterface = requestListenerInterfaceFromString(componentInfo.getListenerInterface());
+		}
+
+		if (listenerInterface != null)
+		{
+			PageAndComponentProvider provider = new PageAndComponentProvider(pageInfo.getPageId(),
+				pageClass, pageParameters, renderCount, componentInfo.getComponentPath());
+
+			provider.setPageSource(getContext());
+
+			return new ListenerInterfaceRequestHandler(provider, listenerInterface,
+				componentInfo.getBehaviorId());
+		}
+		else
+		{
+			if (logger.isWarnEnabled())
+			{
+				if (componentInfo != null)
+				{
+					logger.warn("Unknown listener interface '{}'",
+						componentInfo.getListenerInterface());
+				}
+				else
+				{
+					logger.warn("Cannot extract the listener interface for PageComponentInfo: '{}'" +
+						pageComponentInfo);
+				}
+			}
+			return null;
+		}
+	}
+
+	/**
+	 * @see org.apache.wicket.request.IRequestMapper#mapRequest(org.apache.wicket.request.Request)
+	 */
+	@Override
+	public IRequestHandler mapRequest(Request request)
+	{
+		UrlInfo urlInfo = parseRequest(request);
+
+		// check if the URL is long enough and starts with the proper segments
+		if (urlInfo != null)
+		{
+			PageComponentInfo info = urlInfo.getPageComponentInfo();
+			Class<? extends IRequestablePage> pageClass = urlInfo.getPageClass();
+			PageParameters pageParameters = urlInfo.getPageParameters();
+
+			if (info == null || info.getPageInfo().getPageId() == null)
+			{
+				// if there are is no page instance information (only page map name - optionally)
+				// then this is a simple bookmarkable URL
+				return processBookmarkable(pageClass, pageParameters);
+			}
+			else if (info.getPageInfo().getPageId() != null && info.getComponentInfo() == null)
+			{
+				// if there is page instance information in the URL but no component and listener
+				// interface then this is a hybrid URL - we need to try to reuse existing page
+				// instance
+				return processHybrid(info.getPageInfo(), pageClass, pageParameters, null);
+			}
+			else if (info.getComponentInfo() != null)
+			{
+				// with both page instance and component+listener this is a listener interface URL
+				return processListener(info, pageClass, pageParameters);
+			}
+		}
+		return null;
+	}
+
+	protected boolean checkPageInstance(IRequestablePage page)
+	{
+		return page != null && checkPageClass(page.getClass());
+	}
+
+	protected boolean checkPageClass(Class<? extends IRequestablePage> pageClass)
+	{
+		return true;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Url mapHandler(IRequestHandler requestHandler)
+	{
+		// TODO see if we can refactor this to remove dependency on instanceof checks below and
+		// eliminate the need for IRequestHandlerDelegate
+		while (requestHandler instanceof IRequestHandlerDelegate)
+		{
+			requestHandler = ((IRequestHandlerDelegate)requestHandler).getDelegateHandler();
+		}
+
+		if (requestHandler instanceof BookmarkablePageRequestHandler)
+		{
+			// simple bookmarkable URL with no page instance information
+			BookmarkablePageRequestHandler handler = (BookmarkablePageRequestHandler)requestHandler;
+
+			if (!checkPageClass(handler.getPageClass()))
+			{
+				return null;
+			}
+
+			PageInfo info = new PageInfo();
+			UrlInfo urlInfo = new UrlInfo(new PageComponentInfo(info, null),
+				handler.getPageClass(), handler.getPageParameters());
+
+			return buildUrl(urlInfo);
+		}
+		else if (requestHandler instanceof RenderPageRequestHandler)
+		{
+			// possibly hybrid URL - bookmarkable URL with page instance information
+			// but only allowed if the page was created by bookmarkable URL
+
+			RenderPageRequestHandler handler = (RenderPageRequestHandler)requestHandler;
+
+			if (!checkPageClass(handler.getPageClass()))
+			{
+				return null;
+			}
+
+			if (handler.getPageProvider().isNewPageInstance())
+			{
+				// no existing page instance available, don't bother creating new page instance
+				PageInfo info = new PageInfo();
+				UrlInfo urlInfo = new UrlInfo(new PageComponentInfo(info, null),
+					handler.getPageClass(), handler.getPageParameters());
+
+				return buildUrl(urlInfo);
+			}
+
+			IRequestablePage page = handler.getPage();
+
+			if (checkPageInstance(page) &&
+				(!pageMustHaveBeenCreatedBookmarkable() || page.wasCreatedBookmarkable()))
+			{
+				PageInfo info = null;
+				if (!page.isPageStateless())
+				{
+					info = new PageInfo(page.getPageId());
+				}
+				PageComponentInfo pageComponentInfo = info != null ? new PageComponentInfo(info,
+					null) : null;
+
+				UrlInfo urlInfo = new UrlInfo(pageComponentInfo, page.getClass(),
+					handler.getPageParameters());
+				return buildUrl(urlInfo);
+			}
+			else
+			{
+				return null;
+			}
+
+		}
+		else if (requestHandler instanceof BookmarkableListenerInterfaceRequestHandler)
+		{
+			// listener interface URL with page class information
+			BookmarkableListenerInterfaceRequestHandler handler = (BookmarkableListenerInterfaceRequestHandler)requestHandler;
+			Class<? extends IRequestablePage> pageClass = handler.getPageClass();
+
+			if (!checkPageClass(pageClass))
+			{
+				return null;
+			}
+
+			Integer renderCount = null;
+			if (handler.getListenerInterface().isIncludeRenderCount())
+			{
+				renderCount = handler.getRenderCount();
+			}
+
+			PageInfo pageInfo = new PageInfo(handler.getPageId());
+			ComponentInfo componentInfo = new ComponentInfo(renderCount,
+				requestListenerInterfaceToString(handler.getListenerInterface()),
+				handler.getComponentPath(), handler.getBehaviorIndex());
+
+			UrlInfo urlInfo = new UrlInfo(new PageComponentInfo(pageInfo, componentInfo),
+				pageClass, handler.getPageParameters());
+			return buildUrl(urlInfo);
+		}
+
+		return null;
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/1257c03d/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/AbstractComponentMapper.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/AbstractComponentMapper.java b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/AbstractComponentMapper.java
new file mode 100644
index 0000000..fd75b4b
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/AbstractComponentMapper.java
@@ -0,0 +1,157 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.wicket.core.request.mapper;
+
+import org.apache.wicket.Application;
+import org.apache.wicket.RequestListenerInterface;
+import org.apache.wicket.request.IRequestMapper;
+import org.apache.wicket.request.Url;
+import org.apache.wicket.request.Url.QueryParameter;
+import org.apache.wicket.request.component.IRequestablePage;
+import org.apache.wicket.request.mapper.AbstractMapper;
+import org.apache.wicket.request.mapper.info.PageComponentInfo;
+import org.apache.wicket.util.lang.Args;
+import org.apache.wicket.core.util.lang.WicketObjects;
+import org.apache.wicket.util.string.Strings;
+
+/**
+ * Convenience class for implementing page/components related encoders.
+ *
+ * @author Matej Knopp
+ */
+public abstract class AbstractComponentMapper extends AbstractMapper implements IRequestMapper
+{
+	/**
+	 * Construct.
+	 */
+	public AbstractComponentMapper()
+	{
+	}
+
+	protected IMapperContext getContext()
+	{
+		return Application.get().getMapperContext();
+	}
+
+	/**
+	 * Converts the specified listener interface to String.
+	 *
+	 * @param listenerInterface
+	 * @return listenerInterface name as string
+	 */
+	protected String requestListenerInterfaceToString(RequestListenerInterface listenerInterface)
+	{
+		Args.notNull(listenerInterface, "listenerInterface");
+
+		return getContext().requestListenerInterfaceToString(listenerInterface);
+	}
+
+	/**
+	 * Creates listener interface from the specified string
+	 *
+	 * @param interfaceName
+	 * @return listener interface
+	 */
+	protected RequestListenerInterface requestListenerInterfaceFromString(String interfaceName)
+	{
+		Args.notEmpty(interfaceName, "interfaceName");
+
+		return getContext().requestListenerInterfaceFromString(interfaceName);
+	}
+
+	/**
+	 * Extracts the {@link PageComponentInfo} from the URL. The {@link PageComponentInfo} is encoded
+	 * as the very first query parameter and the parameter consists of name only (no value).
+	 *
+	 * @param url
+	 *
+	 * @return PageComponentInfo instance if one was encoded in URL, <code>null</code> otherwise.
+	 */
+	protected PageComponentInfo getPageComponentInfo(final Url url)
+	{
+		if (url == null)
+		{
+			throw new IllegalStateException("Argument 'url' may not be null.");
+		}
+		else
+		{
+			for (QueryParameter queryParameter : url.getQueryParameters())
+			{
+				if (Strings.isEmpty(queryParameter.getValue()))
+				{
+					PageComponentInfo pageComponentInfo = PageComponentInfo.parse(queryParameter.getName());
+					if (pageComponentInfo != null)
+					{
+						return pageComponentInfo;
+					}
+				}
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * Encodes the {@link PageComponentInfo} instance as the first query string parameter to the
+	 * URL.
+	 *
+	 * @param url
+	 * @param info
+	 */
+	protected void encodePageComponentInfo(Url url, PageComponentInfo info)
+	{
+		Args.notNull(url, "url");
+
+		if (info != null)
+		{
+			String s = info.toString();
+			if (!Strings.isEmpty(s))
+			{
+				QueryParameter parameter = new QueryParameter(s, "");
+				url.getQueryParameters().add(parameter);
+			}
+		}
+	}
+
+	/**
+	 * Loads page class with given name.
+	 *
+	 * @param name
+	 * @return class
+	 */
+	protected Class<? extends IRequestablePage> getPageClass(String name)
+	{
+		Args.notEmpty(name, "name");
+
+		return WicketObjects.resolveClass(name);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * Removes the first query parameter only if {@link PageComponentInfo#parse(String)} returns
+	 * non-null instance
+	 */
+	@Override
+	protected void removeMetaParameter(final Url urlCopy)
+	{
+		String pageComponentInfoCandidate = urlCopy.getQueryParameters().get(0).getName();
+		if (PageComponentInfo.parse(pageComponentInfoCandidate) != null)
+		{
+			urlCopy.getQueryParameters().remove(0);
+		}
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/1257c03d/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/AbstractResourceReferenceMapper.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/AbstractResourceReferenceMapper.java b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/AbstractResourceReferenceMapper.java
new file mode 100644
index 0000000..65e5efb
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/AbstractResourceReferenceMapper.java
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.wicket.core.request.mapper;
+
+import java.util.Locale;
+
+import org.apache.wicket.request.Url;
+import org.apache.wicket.request.Url.QueryParameter;
+import org.apache.wicket.request.resource.ResourceReference;
+import org.apache.wicket.util.lang.Args;
+import org.apache.wicket.util.string.Strings;
+
+/**
+ * Base class for encoding and decoding {@link ResourceReference}s
+ *
+ * @author Matej Knopp
+ */
+public abstract class AbstractResourceReferenceMapper extends AbstractComponentMapper
+{
+
+	protected final String encodeResourceReferenceAttributes(
+		ResourceReference.UrlAttributes attributes)
+	{
+		if (attributes == null ||
+			(attributes.getLocale() == null && attributes.getStyle() == null && attributes.getVariation() == null))
+		{
+			return null;
+		}
+		else
+		{
+			StringBuilder res = new StringBuilder();
+			if (attributes.getLocale() != null)
+			{
+				res.append(attributes.getLocale().toString());
+			}
+			boolean styleEmpty = Strings.isEmpty(attributes.getStyle());
+			if (!styleEmpty)
+			{
+				res.append('-');
+				res.append(attributes.getStyle());
+			}
+			if (!Strings.isEmpty(attributes.getVariation()))
+			{
+				if (styleEmpty)
+				{
+					res.append("--");
+				}
+				else
+				{
+					res.append('-');
+				}
+				res.append(attributes.getVariation());
+			}
+			return res.toString();
+		}
+	}
+
+	private static String nonEmpty(String s)
+	{
+		if (Strings.isEmpty(s))
+		{
+			return null;
+		}
+		else
+		{
+			return s;
+		}
+	}
+
+	protected final ResourceReference.UrlAttributes decodeResourceReferenceAttributes(
+		String attributes)
+	{
+		Locale locale = null;
+		String style = null;
+		String variation = null;
+
+		if (!Strings.isEmpty(attributes))
+		{
+			String split[] = attributes.split("-", 3);
+			locale = parseLocale(split[0]);
+			if (split.length == 2)
+			{
+				style = nonEmpty(split[1]);
+			}
+			else if (split.length == 3)
+			{
+				style = nonEmpty(split[1]);
+				variation = nonEmpty(split[2]);
+			}
+		}
+		return new ResourceReference.UrlAttributes(locale, style, variation);
+	}
+
+	private static Locale parseLocale(String locale)
+	{
+		if (Strings.isEmpty(locale))
+		{
+			return null;
+		}
+		else
+		{
+			String parts[] = locale.toLowerCase().split("_", 3);
+			if (parts.length == 1)
+			{
+				return new Locale(parts[0]);
+			}
+			else if (parts.length == 2)
+			{
+				return new Locale(parts[0], parts[1]);
+			}
+			else if (parts.length == 3)
+			{
+				return new Locale(parts[0], parts[1], parts[2]);
+			}
+			else
+			{
+				return null;
+			}
+		}
+	}
+
+	protected void encodeResourceReferenceAttributes(Url url, ResourceReference reference)
+	{
+		String encoded = encodeResourceReferenceAttributes(reference.getUrlAttributes());
+		if (!Strings.isEmpty(encoded))
+		{
+			url.getQueryParameters().add(new Url.QueryParameter(encoded, ""));
+		}
+	}
+
+	protected ResourceReference.UrlAttributes getResourceReferenceAttributes(Url url)
+	{
+		Args.notNull(url, "url");
+
+		if (url.getQueryParameters().size() > 0)
+		{
+			QueryParameter param = url.getQueryParameters().get(0);
+			if (Strings.isEmpty(param.getValue()))
+			{
+				return decodeResourceReferenceAttributes(param.getName());
+			}
+		}
+		return new ResourceReference.UrlAttributes(null, null, null);
+	}
+
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * Remove the first parameter because it brings meta information like locale
+	 */
+	@Override
+	protected void removeMetaParameter(final Url urlCopy)
+	{
+		urlCopy.getQueryParameters().remove(0);
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/1257c03d/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/BasicResourceReferenceMapper.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/BasicResourceReferenceMapper.java b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/BasicResourceReferenceMapper.java
new file mode 100755
index 0000000..6c0317d
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/BasicResourceReferenceMapper.java
@@ -0,0 +1,256 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.wicket.core.request.mapper;
+
+import java.util.List;
+import java.util.StringTokenizer;
+
+import org.apache.wicket.request.IRequestHandler;
+import org.apache.wicket.request.Request;
+import org.apache.wicket.request.Url;
+import org.apache.wicket.request.handler.resource.ResourceReferenceRequestHandler;
+import org.apache.wicket.request.mapper.parameter.IPageParametersEncoder;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+import org.apache.wicket.request.resource.IResource;
+import org.apache.wicket.request.resource.MetaInfStaticResourceReference;
+import org.apache.wicket.request.resource.ResourceReference;
+import org.apache.wicket.request.resource.caching.IResourceCachingStrategy;
+import org.apache.wicket.request.resource.caching.IStaticCacheableResource;
+import org.apache.wicket.request.resource.caching.ResourceUrl;
+import org.apache.wicket.util.IProvider;
+import org.apache.wicket.core.util.lang.WicketObjects;
+import org.apache.wicket.util.string.Strings;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Generic {@link ResourceReference} encoder that encodes and decodes non-mounted
+ * {@link ResourceReference}s.
+ * <p>
+ * Decodes and encodes the following URLs:
+ *
+ * <pre>
+ *    /wicket/resource/org.apache.wicket.ResourceScope/name
+ *    /wicket/resource/org.apache.wicket.ResourceScope/name?en
+ *    /wicket/resource/org.apache.wicket.ResourceScope/name?-style
+ *    /wicket/resource/org.apache.wicket.ResourceScope/resource/name.xyz?en_EN-style
+ * </pre>
+ *
+ * @author Matej Knopp
+ * @author igor.vaynberg
+ * @author Peter Ertl
+ */
+class BasicResourceReferenceMapper extends AbstractResourceReferenceMapper
+{
+	private static final Logger log = LoggerFactory.getLogger(BasicResourceReferenceMapper.class);
+
+	private final IPageParametersEncoder pageParametersEncoder;
+
+	/** resource caching strategy */
+	private final IProvider<? extends IResourceCachingStrategy> cachingStrategy;
+
+	/**
+	 * Construct.
+	 *
+	 * @param pageParametersEncoder
+	 * @param cachingStrategy
+	 */
+	public BasicResourceReferenceMapper(IPageParametersEncoder pageParametersEncoder,
+		IProvider<? extends IResourceCachingStrategy> cachingStrategy)
+	{
+		this.pageParametersEncoder = pageParametersEncoder;
+		this.cachingStrategy = cachingStrategy;
+	}
+
+	@Override
+	public IRequestHandler mapRequest(Request request)
+	{
+		Url url = request.getUrl();
+
+		// extract the PageParameters from URL if there are any
+		PageParameters pageParameters = extractPageParameters(request, url.getSegments().size(),
+			pageParametersEncoder);
+
+		if (url.getSegments().size() >= 4 &&
+			urlStartsWith(url, getContext().getNamespace(), getContext().getResourceIdentifier()))
+		{
+			String className = url.getSegments().get(2);
+			StringBuilder name = new StringBuilder();
+			int segmentsSize = url.getSegments().size();
+			for (int i = 3; i < segmentsSize; ++i)
+			{
+				String segment = url.getSegments().get(i);
+
+				// skip possibly malicious segments
+				if (segment.contains("/"))
+				{
+					return null;
+				}
+
+				// remove caching information
+				if (i + 1 == segmentsSize && Strings.isEmpty(segment) == false)
+				{
+					// The filename + parameters eventually contain caching
+					// related information which needs to be removed
+					ResourceUrl resourceUrl = new ResourceUrl(segment, pageParameters);
+					getCachingStrategy().undecorateUrl(resourceUrl);
+					segment = resourceUrl.getFileName();
+
+					if (Strings.isEmpty(segment))
+					{
+						throw new IllegalStateException("caching strategy returned empty name for " + resourceUrl);
+					}
+				}
+				if (name.length() > 0)
+				{
+					name.append("/");
+				}
+				name.append(segment);
+			}
+
+			ResourceReference.UrlAttributes attributes = getResourceReferenceAttributes(url);
+
+			Class<?> scope = resolveClass(className);
+
+			if (scope != null && scope.getPackage() != null)
+			{
+				ResourceReference res = getContext().getResourceReferenceRegistry()
+					.getResourceReference(scope, name.toString(), attributes.getLocale(),
+						attributes.getStyle(), attributes.getVariation(), true, true);
+
+				if (res != null)
+				{
+					return new ResourceReferenceRequestHandler(res, pageParameters);
+				}
+			}
+		}
+		return null;
+	}
+
+	private IResourceCachingStrategy getCachingStrategy()
+	{
+		return cachingStrategy.get();
+	}
+
+	protected Class<?> resolveClass(String name)
+	{
+		return WicketObjects.resolveClass(name);
+	}
+
+	protected String getClassName(Class<?> scope)
+	{
+		return scope.getName();
+	}
+
+	@Override
+	public Url mapHandler(IRequestHandler requestHandler)
+	{
+		if (requestHandler instanceof ResourceReferenceRequestHandler)
+		{
+			ResourceReferenceRequestHandler referenceRequestHandler = (ResourceReferenceRequestHandler)requestHandler;
+			ResourceReference reference = referenceRequestHandler.getResourceReference();
+
+			Url url;
+
+			if (reference instanceof MetaInfStaticResourceReference)
+			{
+				url = ((MetaInfStaticResourceReference)reference).mapHandler(referenceRequestHandler);
+				// if running on Servlet 3.0 engine url is not null
+				if (url != null)
+				{
+					return url;
+				}
+				// otherwise it has to be served by the standard wicket way
+			}
+
+			url = new Url();
+
+			List<String> segments = url.getSegments();
+			segments.add(getContext().getNamespace());
+			segments.add(getContext().getResourceIdentifier());
+			segments.add(getClassName(reference.getScope()));
+
+			// setup resource parameters
+			PageParameters parameters = referenceRequestHandler.getPageParameters();
+
+			if (parameters == null)
+			{
+				parameters = new PageParameters();
+			}
+			else
+			{
+				parameters = new PageParameters(parameters);
+
+				// need to remove indexed parameters otherwise the URL won't be able to decode
+				parameters.clearIndexed();
+			}
+			encodeResourceReferenceAttributes(url, reference);
+
+			StringTokenizer tokens = new StringTokenizer(reference.getName(), "/");
+
+			while (tokens.hasMoreTokens())
+			{
+				String token = tokens.nextToken();
+
+				// on the last component of the resource path
+				if (tokens.hasMoreTokens() == false && Strings.isEmpty(token) == false)
+				{
+					final IResource resource = reference.getResource();
+
+					// apply caching if required
+					if (resource instanceof IStaticCacheableResource)
+					{
+						// add caching related information to filename + query parameters
+						final IStaticCacheableResource cacheable = (IStaticCacheableResource)resource;
+						final ResourceUrl resourceUrl = new ResourceUrl(token, parameters);
+						getCachingStrategy().decorateUrl(resourceUrl, cacheable);
+						token = resourceUrl.getFileName();
+
+						if (Strings.isEmpty(token))
+						{
+							throw new IllegalStateException("caching strategy returned empty name for " + resource);
+						}
+					}
+				}
+				segments.add(token);
+			}
+
+			if (parameters.isEmpty() == false)
+			{
+				url = encodePageParameters(url, parameters, pageParametersEncoder);
+			}
+
+			return url;
+		}
+		return null;
+	}
+
+	@Override
+	public int getCompatibilityScore(Request request)
+	{
+		Url url = request.getUrl();
+
+		int score = -1;
+		if (url.getSegments().size() >= 4 &&
+			urlStartsWith(url, getContext().getNamespace(), getContext().getResourceIdentifier()))
+		{
+			score = 1;
+		}
+
+		return score;
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/1257c03d/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/BookmarkableMapper.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/BookmarkableMapper.java b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/BookmarkableMapper.java
new file mode 100644
index 0000000..22ff3a7
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/BookmarkableMapper.java
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.wicket.core.request.mapper;
+
+import org.apache.wicket.Application;
+import org.apache.wicket.request.Request;
+import org.apache.wicket.request.Url;
+import org.apache.wicket.request.component.IRequestablePage;
+import org.apache.wicket.request.mapper.info.PageComponentInfo;
+import org.apache.wicket.request.mapper.parameter.IPageParametersEncoder;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+import org.apache.wicket.request.mapper.parameter.PageParametersEncoder;
+import org.apache.wicket.util.lang.Args;
+
+/**
+ * Decodes and encodes the following URLs:
+ *
+ * <pre>
+ *  Page Class - Render (BookmarkablePageRequestHandler)
+ *  /wicket/bookmarkable/org.apache.wicket.MyPage
+ *  (will redirect to hybrid alternative if page is not stateless)
+ *
+ *  Page Instance - Render Hybrid (RenderPageRequestHandler for pages that were created using bookmarkable URLs)
+ *  /wicket/bookmarkable/org.apache.wicket.MyPage?2
+ *
+ *  Page Instance - Bookmarkable Listener (BookmarkableListenerInterfaceRequestHandler)
+ *  /wicket/bookmarkable/org.apache.wicket.MyPage?2-click-foo-bar-baz
+ *  /wicket/bookmarkable/org.apache.wicket.MyPage?2-click.1-foo-bar-baz (1 is behavior index)
+ *  (these will redirect to hybrid if page is not stateless)
+ * </pre>
+ *
+ * @author Matej Knopp
+ */
+public class BookmarkableMapper extends AbstractBookmarkableMapper
+{
+	private final IPageParametersEncoder pageParametersEncoder;
+
+	/**
+	 * Construct.
+	 *
+	 * @param pageParametersEncoder
+	 */
+	public BookmarkableMapper(IPageParametersEncoder pageParametersEncoder)
+	{
+		Args.notNull(pageParametersEncoder, "pageParametersEncoder");
+
+		this.pageParametersEncoder = pageParametersEncoder;
+	}
+
+	/**
+	 * Construct.
+	 */
+	public BookmarkableMapper()
+	{
+		this(new PageParametersEncoder());
+	}
+
+	/**
+	 * @see org.apache.wicket.request.mapper.AbstractBookmarkableMapper#buildUrl(org.apache.wicket.request.mapper.AbstractBookmarkableMapper.UrlInfo)
+	 */
+	@Override
+	protected Url buildUrl(UrlInfo info)
+	{
+		Url url = new Url();
+		url.getSegments().add(getContext().getNamespace());
+		url.getSegments().add(getContext().getBookmarkableIdentifier());
+		url.getSegments().add(info.getPageClass().getName());
+
+		encodePageComponentInfo(url, info.getPageComponentInfo());
+
+		return encodePageParameters(url, info.getPageParameters(), pageParametersEncoder);
+	}
+
+	/**
+	 * @see org.apache.wicket.request.mapper.AbstractBookmarkableMapper#parseRequest(org.apache.wicket.request.Request)
+	 */
+	@Override
+	protected UrlInfo parseRequest(Request request)
+	{
+		if (Application.exists())
+		{
+			if (Application.get().getSecuritySettings().getEnforceMounts())
+			{
+				return null;
+			}
+		}
+
+		Url url = request.getUrl();
+		if (matches(url))
+		{
+			// try to extract page and component information from URL
+			PageComponentInfo info = getPageComponentInfo(url);
+
+			// load the page class
+			String className = url.getSegments().get(2);
+			Class<? extends IRequestablePage> pageClass = getPageClass(className);
+
+			if (pageClass != null && IRequestablePage.class.isAssignableFrom(pageClass))
+			{
+
+				// extract the PageParameters from URL if there are any
+				PageParameters pageParameters = extractPageParameters(request, 3,
+					pageParametersEncoder);
+
+				return new UrlInfo(info, pageClass, pageParameters);
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * @see org.apache.wicket.request.mapper.AbstractBookmarkableMapper#pageMustHaveBeenCreatedBookmarkable()
+	 */
+	@Override
+	protected boolean pageMustHaveBeenCreatedBookmarkable()
+	{
+		return true;
+	}
+
+	/**
+	 * @see org.apache.wicket.request.mapper.AbstractBookmarkableMapper#getCompatibilityScore(org.apache.wicket.request.Request)
+	 */
+	@Override
+	public int getCompatibilityScore(Request request)
+	{
+		int score = 0;
+		Url url = request.getUrl();
+		if (matches(url))
+		{
+			score = Integer.MAX_VALUE;
+		}
+		return score;
+	}
+
+	private boolean matches(final Url url)
+	{
+		return (url.getSegments().size() >= 3 && urlStartsWith(url, getContext().getNamespace(),
+			getContext().getBookmarkableIdentifier()));
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/1257c03d/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/BufferedResponseMapper.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/BufferedResponseMapper.java b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/BufferedResponseMapper.java
new file mode 100644
index 0000000..f04e674
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/BufferedResponseMapper.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.wicket.core.request.mapper;
+
+import org.apache.wicket.Application;
+import org.apache.wicket.Session;
+import org.apache.wicket.protocol.http.BufferedWebResponse;
+import org.apache.wicket.protocol.http.WebApplication;
+import org.apache.wicket.request.IRequestCycle;
+import org.apache.wicket.request.IRequestHandler;
+import org.apache.wicket.request.IRequestMapper;
+import org.apache.wicket.request.Request;
+import org.apache.wicket.request.Url;
+import org.apache.wicket.request.cycle.RequestCycle;
+import org.apache.wicket.core.request.handler.BufferedResponseRequestHandler;
+import org.apache.wicket.session.ISessionStore;
+import org.apache.wicket.util.string.Strings;
+
+/**
+ * Encoder that intercepts requests for which there is already stored buffer with rendered data.
+ *
+ * @author Matej Knopp
+ */
+public class BufferedResponseMapper implements IRequestMapper
+{
+	/**
+	 * Construct.
+	 */
+	public BufferedResponseMapper()
+	{
+	}
+
+	/**
+	 * @return the current session id for stateful pages and <code>null</code> for stateless pages
+	 *         and non-http threads
+	 */
+	protected String getSessionId()
+	{
+		String sessionId = null;
+
+		if (Application.exists() && RequestCycle.get() != null)
+		{
+			ISessionStore sessionStore = Application.get().getSessionStore();
+			IRequestCycle requestCycle = RequestCycle.get();
+			Session session = sessionStore.lookup(requestCycle.getRequest());
+			if (session != null)
+			{
+				sessionId = session.getId();
+			}
+		}
+
+		return sessionId;
+	}
+
+	protected boolean hasBufferedResponse(Url url)
+	{
+		String sessionId = getSessionId();
+		boolean hasResponse = false;
+		if (Strings.isEmpty(sessionId) == false)
+		{
+			hasResponse = WebApplication.get().hasBufferedResponse(sessionId, url);
+		}
+		return hasResponse;
+	}
+
+	protected BufferedWebResponse getAndRemoveBufferedResponse(Url url)
+	{
+		String sessionId = getSessionId();
+		BufferedWebResponse response = null;
+		if (Strings.isEmpty(sessionId) == false)
+		{
+			response = WebApplication.get().getAndRemoveBufferedResponse(sessionId, url);
+		}
+		return response;
+	}
+
+	private Request getRequest(Request original)
+	{
+		// The buffers are stored under "real" URL which can be different
+		// than the URL handlers get due to global URL pre/postprocessing
+		// (i.e. prepending URL with language segment).
+		// Because of that we need find out the real URL from request cycle
+
+		if (RequestCycle.get() != null)
+		{
+			return RequestCycle.get().getRequest();
+		}
+		else
+		{
+			return original;
+		}
+	}
+
+	/**
+	 * @see org.apache.wicket.request.IRequestMapper#mapRequest(org.apache.wicket.request.Request)
+	 */
+	@Override
+	public IRequestHandler mapRequest(Request request)
+	{
+		request = getRequest(request);
+
+		BufferedWebResponse response = getAndRemoveBufferedResponse(request.getUrl());
+		if (response != null)
+		{
+			return new BufferedResponseRequestHandler(response);
+		}
+		else
+		{
+			return null;
+		}
+	}
+
+	/**
+	 * @see org.apache.wicket.request.IRequestMapper#mapHandler(org.apache.wicket.request.IRequestHandler)
+	 */
+	@Override
+	public Url mapHandler(IRequestHandler requestHandler)
+	{
+		return null;
+	}
+
+	/**
+	 * @see org.apache.wicket.request.IRequestMapper#getCompatibilityScore(org.apache.wicket.request.Request)
+	 */
+	@Override
+	public int getCompatibilityScore(Request request)
+	{
+		request = getRequest(request);
+
+		if (hasBufferedResponse(request.getUrl()))
+		{
+			return Integer.MAX_VALUE;
+		}
+		else
+		{
+			return 0;
+		}
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/1257c03d/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/CryptoMapper.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/CryptoMapper.java b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/CryptoMapper.java
new file mode 100755
index 0000000..40b0efa
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/CryptoMapper.java
@@ -0,0 +1,286 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.wicket.core.request.mapper;
+
+import java.util.List;
+
+import org.apache.wicket.Application;
+import org.apache.wicket.request.IRequestHandler;
+import org.apache.wicket.request.IRequestMapper;
+import org.apache.wicket.request.Request;
+import org.apache.wicket.request.Url;
+import org.apache.wicket.util.IProvider;
+import org.apache.wicket.util.crypt.ICrypt;
+import org.apache.wicket.util.lang.Args;
+import org.apache.wicket.util.string.Strings;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Request mapper that encrypts urls generated by another mapper. The original URL (both segments
+ * and parameters) is encrypted and is represented as URL segment. To be able to handle relative
+ * URLs for images in .css file the same amount of URL segments that the original URL had are
+ * appended to the encrypted URL. Each segment has a precise 5 character value, calculated using a
+ * checksum. This helps in calculating the relative distance from the original URL. When a URL is
+ * returned by the browser, we iterate through these checksummed placeholder URL segments. If the
+ * segment matches the expected checksum, then the segment it deemed to be the corresponding segment
+ * in the encrypted URL. If the segment does not match the expected checksum, then the segment is
+ * deemed a plain text sibling of the corresponding segment in the encrypted URL, and all subsequent
+ * segments are considered plain text children of the current segment.
+ *
+ *
+ * @author igor.vaynberg
+ * @author Jesse Long
+ * @author svenmeier
+ */
+public class CryptoMapper implements IRequestMapper
+{
+	private static final Logger log = LoggerFactory.getLogger(CryptoMapper.class);
+
+	private final IRequestMapper wrappedMapper;
+	private final IProvider<ICrypt> cryptProvider;
+
+	/**
+	 * Construct.
+	 *
+	 * @param wrappedMapper
+	 *            the non-crypted request mapper
+	 * @param application
+	 *            the current application
+	 */
+	public CryptoMapper(final IRequestMapper wrappedMapper, final Application application)
+	{
+		this(wrappedMapper, new ApplicationCryptProvider(application));
+	}
+
+	/**
+	 * Construct.
+	 *
+	 * @param wrappedMapper
+	 *            the non-crypted request mapper
+	 * @param cryptProvider
+	 *            the custom crypt provider
+	 */
+	public CryptoMapper(final IRequestMapper wrappedMapper, final IProvider<ICrypt> cryptProvider)
+	{
+		this.wrappedMapper = Args.notNull(wrappedMapper, "wrappedMapper");
+		this.cryptProvider = Args.notNull(cryptProvider, "cryptProvider");
+	}
+
+	@Override
+	public int getCompatibilityScore(final Request request)
+	{
+		return 0;
+	}
+
+	@Override
+	public Url mapHandler(final IRequestHandler requestHandler)
+	{
+		final Url url = wrappedMapper.mapHandler(requestHandler);
+
+		if (url == null)
+		{
+			return null;
+		}
+
+		return encryptUrl(url);
+	}
+
+	@Override
+	public IRequestHandler mapRequest(final Request request)
+	{
+		Url url = decryptUrl(request, request.getUrl());
+
+		if (url == null)
+		{
+			return null;
+		}
+
+		return wrappedMapper.mapRequest(request.cloneWithUrl(url));
+	}
+
+	/**
+	 * @return the {@link ICrypt} implementation that may be used to encrypt/decrypt {@link Url}'s
+	 *         segments and/or query string
+	 */
+	protected final ICrypt getCrypt()
+	{
+		return cryptProvider.get();
+	}
+
+	/**
+	 * @return the wrapped root request mapper
+	 */
+	protected final IRequestMapper getWrappedMapper()
+	{
+		return wrappedMapper;
+	}
+
+	private Url encryptUrl(final Url url)
+	{
+		if (url.getSegments().isEmpty() && url.getQueryParameters().isEmpty())
+		{
+			return url;
+		}
+		String encryptedUrlString = getCrypt().encryptUrlSafe(url.toString());
+
+		Url encryptedUrl = new Url(url.getCharset());
+		encryptedUrl.getSegments().add(encryptedUrlString);
+
+		int numberOfSegments = url.getSegments().size();
+		HashedSegmentGenerator generator = new HashedSegmentGenerator(encryptedUrlString);
+		for (int segNo = 0; segNo < numberOfSegments; segNo++)
+		{
+			encryptedUrl.getSegments().add(generator.next());
+		}
+		return encryptedUrl;
+	}
+
+	private Url decryptUrl(final Request request, final Url encryptedUrl)
+	{
+		if (encryptedUrl.getSegments().isEmpty())
+		{
+			return encryptedUrl;
+		}
+
+		List<String> encryptedSegments = encryptedUrl.getSegments();
+		if (encryptedSegments.size() < 1)
+		{
+			return null;
+		}
+
+		Url url = new Url(request.getCharset());
+		try
+		{
+			String encryptedUrlString = encryptedSegments.get(0);
+			if (Strings.isEmpty(encryptedUrlString))
+			{
+				return null;
+			}
+
+			String decryptedUrl = getCrypt().decryptUrlSafe(encryptedUrlString);
+			if (decryptedUrl == null)
+			{
+				return null;
+			}
+			Url originalUrl = Url.parse(decryptedUrl, request.getCharset());
+
+			int originalNumberOfSegments = originalUrl.getSegments().size();
+			int encryptedNumberOfSegments = encryptedUrl.getSegments().size();
+
+			HashedSegmentGenerator generator = new HashedSegmentGenerator(encryptedUrlString);
+			int segNo = 1;
+			for (; segNo < encryptedNumberOfSegments; segNo++)
+			{
+				if (segNo > originalNumberOfSegments)
+				{
+					break;
+				}
+
+				String next = generator.next();
+				String encryptedSegment = encryptedSegments.get(segNo);
+				if (!next.equals(encryptedSegment))
+				{
+					break;
+				}
+
+				// unmodified segment
+				url.getSegments().add(originalUrl.getSegments().get(segNo - 1));
+			}
+			for (; segNo < encryptedNumberOfSegments; segNo++)
+			{
+				// modified or additional segment
+				url.getSegments().add(encryptedUrl.getSegments().get(segNo));
+			}
+
+			url.getQueryParameters().addAll(originalUrl.getQueryParameters());
+		}
+		catch (Exception e)
+		{
+			log.error("Error decrypting URL", e);
+			url = null;
+		}
+
+		return url;
+	}
+
+	private static class ApplicationCryptProvider implements IProvider<ICrypt>
+	{
+		private final Application application;
+
+		public ApplicationCryptProvider(final Application application)
+		{
+			this.application = application;
+		}
+
+		@Override
+		public ICrypt get()
+		{
+			return application.getSecuritySettings().getCryptFactory().newCrypt();
+		}
+	}
+
+	/**
+	 * A generator of hashed segments.
+	 */
+	private static class HashedSegmentGenerator
+	{
+		private char[] characters;
+
+		private int hash = 0;
+
+		public HashedSegmentGenerator(String string)
+		{
+			characters = string.toCharArray();
+		}
+
+		/**
+		 * Generate the next segment
+		 *
+		 * @return segment
+		 */
+		public String next()
+		{
+			char a = characters[Math.abs(hash % characters.length)];
+			hash++;
+			char b = characters[Math.abs(hash % characters.length)];
+			hash++;
+			char c = characters[Math.abs(hash % characters.length)];
+
+			String segment = "" + a + b + c;
+			hash = hashString(segment);
+
+			segment += String.format("%02x", Math.abs(hash % 256));
+			hash = hashString(segment);
+
+			return segment;
+		}
+
+		private int hashString(final String str)
+		{
+			int hash = 97;
+
+			for (char c : str.toCharArray())
+			{
+				int i = c;
+				hash = 47 * hash + i;
+			}
+
+			return hash;
+		}
+	}
+}
\ No newline at end of file