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/07 09:39:03 UTC

[5/19] 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/53f07873/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/HomePageMapper.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/HomePageMapper.java b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/HomePageMapper.java
new file mode 100644
index 0000000..1525420
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/HomePageMapper.java
@@ -0,0 +1,112 @@
+/*
+ * 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.request.Request;
+import org.apache.wicket.request.Url;
+import org.apache.wicket.request.component.IRequestablePage;
+import org.apache.wicket.request.mapper.parameter.IPageParametersEncoder;
+import org.apache.wicket.util.ClassProvider;
+
+/**
+ * A mapper that is used when a request to the home page ("/") is made
+ */
+public class HomePageMapper extends MountedMapper
+{
+
+	/**
+	 * Construct.
+	 * 
+	 * @param pageClass
+	 *            the class of the page which should handle requests to "/"
+	 */
+	public HomePageMapper(final Class<? extends IRequestablePage> pageClass)
+	{
+		super("/", pageClass);
+	}
+
+	/**
+	 * Construct.
+	 * 
+	 * @param pageClassProvider
+	 *            the class of the page which should handle requests to "/"
+	 */
+	public HomePageMapper(ClassProvider<? extends IRequestablePage> pageClassProvider)
+	{
+		super("/", pageClassProvider);
+	}
+
+	/**
+	 * Construct.
+	 * 
+	 * @param pageClass
+	 *            the class of the page which should handle requests to "/"
+	 * @param pageParametersEncoder
+	 *            the encoder that will be used to encode/decode the page parameters
+	 */
+	public HomePageMapper(Class<? extends IRequestablePage> pageClass,
+		IPageParametersEncoder pageParametersEncoder)
+	{
+		super("/", pageClass, pageParametersEncoder);
+	}
+
+	/**
+	 * Construct.
+	 * 
+	 * @param pageClassProvider
+	 *            the class of the page which should handle requests to "/"
+	 * @param pageParametersEncoder
+	 *            the encoder that will be used to encode/decode the page parameters
+	 */
+	public HomePageMapper(final ClassProvider<? extends IRequestablePage> pageClassProvider,
+		IPageParametersEncoder pageParametersEncoder)
+	{
+		super("/", pageClassProvider, pageParametersEncoder);
+	}
+
+	/**
+	 * Matches only when there are no segments/indexed parameters
+	 * 
+	 * @see org.apache.wicket.request.mapper.AbstractBookmarkableMapper#parseRequest(org.apache.wicket.request.Request)
+	 */
+	@Override
+	protected UrlInfo parseRequest(Request request)
+	{
+		// get canonical url
+		final Url url = request.getUrl().canonical();
+		
+		if (url.getSegments().size() > 0)
+		{
+			// home page cannot have segments/indexed parameters
+			return null;
+		}
+
+		return super.parseRequest(request);
+	}
+
+	/**
+	 * Use this mapper as a last option. Let all other mappers to try to handle the request
+	 * 
+	 * @see org.apache.wicket.request.mapper.MountedMapper#getCompatibilityScore(org.apache.wicket.request.Request)
+	 */
+	@Override
+	public int getCompatibilityScore(Request request)
+	{
+		return Integer.MIN_VALUE + 1;
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/53f07873/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/IMapperContext.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/IMapperContext.java b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/IMapperContext.java
new file mode 100644
index 0000000..ec8f51f
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/IMapperContext.java
@@ -0,0 +1,77 @@
+/*
+ * 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.request.component.IRequestablePage;
+import org.apache.wicket.request.resource.ResourceReferenceRegistry;
+
+/**
+ * Utility interface for providing and creating new page instances.
+ * 
+ * @author Matej Knopp
+ */
+public interface IMapperContext extends IPageSource
+{
+	/**
+	 * @return the namespace for Wicket URLs.
+	 */
+	String getNamespace();
+
+	/**
+	 * @return identifier for non bookmarkable URLs
+	 */
+	String getPageIdentifier();
+
+	/**
+	 * @return identifier for bookmarkable URLs
+	 */
+	String getBookmarkableIdentifier();
+
+	/**
+	 * @return identifier for resources
+	 */
+	String getResourceIdentifier();
+
+	/**
+	 * @return {@link ResourceReferenceRegistry}
+	 */
+	ResourceReferenceRegistry getResourceReferenceRegistry();
+
+	/**
+	 * Returns the listener interface name as string.
+	 * 
+	 * @param listenerInterface
+	 * @return listener interface name as string
+	 */
+	String requestListenerInterfaceToString(RequestListenerInterface listenerInterface);
+
+	/**
+	 * Returns listener interface for the name
+	 * 
+	 * @param interfaceName
+	 * @return listener interface
+	 */
+	RequestListenerInterface requestListenerInterfaceFromString(String interfaceName);
+
+	/**
+	 * Returns the home page class.
+	 * 
+	 * @return home page class
+	 */
+	Class<? extends IRequestablePage> getHomePageClass();
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/53f07873/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/IPageSource.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/IPageSource.java b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/IPageSource.java
new file mode 100644
index 0000000..2f6c93b
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/IPageSource.java
@@ -0,0 +1,49 @@
+/*
+ * 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.request.component.IRequestablePage;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+
+/**
+ * Interface for objects that are capable of getting and creating page instance.
+ * 
+ * @author Matej Knopp
+ */
+public interface IPageSource
+{
+	/**
+	 * Returns existing page instance if the page exists.
+	 * 
+	 * @param pageId
+	 * @return page instance or <code>null</code> if the page does not exist.
+	 */
+	IRequestablePage getPageInstance(int pageId);
+
+	/**
+	 * Creates new page instance of page with given class. The page should be marked as create
+	 * bookmarkable, so subsequent calls to {@link IRequestablePage#wasCreatedBookmarkable()} must
+	 * return <code>true</code>
+	 * 
+	 * @param pageClass
+	 * @param pageParameters
+	 * @return new page instance
+	 */
+	IRequestablePage newPageInstance(Class<? extends IRequestablePage> pageClass,
+		PageParameters pageParameters);
+
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/53f07873/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/MountedMapper.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/MountedMapper.java b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/MountedMapper.java
new file mode 100644
index 0000000..118bfb5
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/MountedMapper.java
@@ -0,0 +1,524 @@
+/*
+ * 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.ArrayList;
+import java.util.List;
+
+import org.apache.wicket.Application;
+import org.apache.wicket.RequestListenerInterface;
+import org.apache.wicket.core.request.handler.ListenerInterfaceRequestHandler;
+import org.apache.wicket.request.IRequestHandler;
+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.ComponentInfo;
+import org.apache.wicket.request.mapper.info.PageComponentInfo;
+import org.apache.wicket.request.mapper.info.PageInfo;
+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.ClassProvider;
+import org.apache.wicket.util.lang.Args;
+import org.apache.wicket.util.string.Strings;
+
+/**
+ * Encoder for mounted URL. The mount path can contain parameter placeholders, i.e.
+ * <code>/mount/${foo}/path</code>. In that case the appropriate segment from the URL will be
+ * accessible as named parameter "foo" in the {@link PageParameters}. Similarly when the URL is
+ * constructed, the second segment will contain the value of the "foo" named page parameter.
+ * Optional parameters are denoted by using a # instead of $: <code>/mount/#{foo}/path/${bar}</code>
+ * has an optional {@code foo} parameter, a fixed {@code /path/} part and a required {@code bar}
+ * parameter. When in doubt, parameters are matched from left to right, where required parameters
+ * are matched before optional parameters, and optional parameters eager (from left to right).
+ * <p>
+ * Decodes and encodes the following URLs:
+ * 
+ * <pre>
+ *  Page Class - Render (BookmarkablePageRequestHandler for mounted pages)
+ *  /mount/point
+ *  (these will redirect to hybrid alternative if page is not stateless)
+ * 
+ *  IPage Instance - Render Hybrid (RenderPageRequestHandler for mounted pages) 
+ *  /mount/point?2
+ * 
+ *  IPage Instance - Bookmarkable Listener (BookmarkableListenerInterfaceRequestHandler for mounted pages) 
+ *  /mount/point?2-click-foo-bar-baz
+ *  /mount/point?2-5.click.1-foo-bar-baz (1 is behavior index, 5 is render count)
+ *  (these will redirect to hybrid if page is not stateless)
+ * </pre>
+ * 
+ * @author Matej Knopp
+ */
+public class MountedMapper extends AbstractBookmarkableMapper
+{
+	private final IPageParametersEncoder pageParametersEncoder;
+
+	private static class MountPathSegment
+	{
+		private int segmentIndex;
+		private String fixedPart;
+		private int minParameters;
+		private int optionalParameters;
+
+		public MountPathSegment(int segmentIndex)
+		{
+			this.segmentIndex = segmentIndex;
+		}
+
+		public void setFixedPart(String fixedPart)
+		{
+			this.fixedPart = fixedPart;
+		}
+
+		public void addRequiredParameter()
+		{
+			minParameters++;
+		}
+
+		public void addOptionalParameter()
+		{
+			optionalParameters++;
+		}
+
+		public int getSegmentIndex()
+		{
+			return segmentIndex;
+		}
+
+		public String getFixedPart()
+		{
+			return fixedPart;
+		}
+
+		public int getMinParameters()
+		{
+			return minParameters;
+		}
+
+		public int getOptionalParameters()
+		{
+			return optionalParameters;
+		}
+
+		public int getMaxParameters()
+		{
+			return getOptionalParameters() + getMinParameters();
+		}
+
+		public int getFixedPartSize()
+		{
+			return getFixedPart() == null ? 0 : 1;
+		}
+
+		@Override
+		public String toString()
+		{
+			return "(" + getSegmentIndex() + ") " + getMinParameters() + "-" + getMaxParameters() +
+				" " + (getFixedPart() == null ? "(end)" : getFixedPart());
+		}
+	}
+
+	private final List<MountPathSegment> pathSegments;
+	private final String[] mountSegments;
+
+	/** bookmarkable page class. */
+	private final ClassProvider<? extends IRequestablePage> pageClassProvider;
+
+	/**
+	 * Construct.
+	 * 
+	 * @param mountPath
+	 * @param pageClass
+	 */
+	public MountedMapper(String mountPath, Class<? extends IRequestablePage> pageClass)
+	{
+		this(mountPath, pageClass, new PageParametersEncoder());
+	}
+
+	/**
+	 * Construct.
+	 * 
+	 * @param mountPath
+	 * @param pageClassProvider
+	 */
+	public MountedMapper(String mountPath,
+		ClassProvider<? extends IRequestablePage> pageClassProvider)
+	{
+		this(mountPath, pageClassProvider, new PageParametersEncoder());
+	}
+
+	/**
+	 * Construct.
+	 * 
+	 * @param mountPath
+	 * @param pageClass
+	 * @param pageParametersEncoder
+	 */
+	public MountedMapper(String mountPath, Class<? extends IRequestablePage> pageClass,
+		IPageParametersEncoder pageParametersEncoder)
+	{
+		this(mountPath, ClassProvider.of(pageClass), pageParametersEncoder);
+	}
+
+	/**
+	 * Construct.
+	 * 
+	 * @param mountPath
+	 * @param pageClassProvider
+	 * @param pageParametersEncoder
+	 */
+	public MountedMapper(String mountPath,
+		ClassProvider<? extends IRequestablePage> pageClassProvider,
+		IPageParametersEncoder pageParametersEncoder)
+	{
+		Args.notEmpty(mountPath, "mountPath");
+		Args.notNull(pageClassProvider, "pageClassProvider");
+		Args.notNull(pageParametersEncoder, "pageParametersEncoder");
+
+		this.pageParametersEncoder = pageParametersEncoder;
+		this.pageClassProvider = pageClassProvider;
+		mountSegments = getMountSegments(mountPath);
+		pathSegments = getPathSegments(mountSegments);
+	}
+
+	private List<MountPathSegment> getPathSegments(String[] segments)
+	{
+		List<MountPathSegment> ret = new ArrayList<MountPathSegment>();
+		int segmentIndex = 0;
+		MountPathSegment curPathSegment = new MountPathSegment(segmentIndex);
+		ret.add(curPathSegment);
+		for (String curSegment : segments)
+		{
+			if (isFixedSegment(curSegment))
+			{
+				curPathSegment.setFixedPart(curSegment);
+				curPathSegment = new MountPathSegment(segmentIndex + 1);
+				ret.add(curPathSegment);
+			}
+			else if (getPlaceholder(curSegment) != null)
+			{
+				curPathSegment.addRequiredParameter();
+			}
+			else
+			{
+				curPathSegment.addOptionalParameter();
+			}
+			segmentIndex++;
+		}
+		return ret;
+	}
+
+	private boolean isFixedSegment(String segment)
+	{
+		return getOptionalPlaceholder(segment) == null && getPlaceholder(segment) == null;
+	}
+
+	/**
+	 * @see AbstractBookmarkableMapper#parseRequest(org.apache.wicket.request.Request)
+	 */
+	@Override
+	protected UrlInfo parseRequest(Request request)
+	{
+		Url url = request.getUrl();
+
+		// when redirect to buffer/render is active and redirectFromHomePage returns true
+		// check mounted class against the home page class. if it matches let wicket redirect
+		// to the mounted URL
+		if (redirectFromHomePage() && checkHomePage(url))
+		{
+			return new UrlInfo(null, getContext().getHomePageClass(), newPageParameters());
+		}
+		// check if the URL starts with the proper segments
+		else if (urlStartsWith(url, mountSegments))
+		{
+			// try to extract page and component information from URL
+			PageComponentInfo info = getPageComponentInfo(url);
+			Class<? extends IRequestablePage> pageClass = getPageClass();
+			PageParameters pageParameters = extractPageParameters(request, url);
+
+			return new UrlInfo(info, pageClass, pageParameters);
+		}
+		else
+		{
+			return null;
+		}
+	}
+
+	/*
+	 * extract the PageParameters from URL if there are any
+	 */
+	private PageParameters extractPageParameters(Request request, Url url)
+	{
+		int[] matchedParameters = getMatchedSegmentSizes(url);
+		int total = 0;
+		for (int curMatchSize : matchedParameters)
+			total += curMatchSize;
+		PageParameters pageParameters = extractPageParameters(request, total, pageParametersEncoder);
+
+		int skippedParameters = 0;
+		for (int pathSegmentIndex = 0; pathSegmentIndex < pathSegments.size(); pathSegmentIndex++)
+		{
+			MountPathSegment curPathSegment = pathSegments.get(pathSegmentIndex);
+			int matchSize = matchedParameters[pathSegmentIndex] - curPathSegment.getFixedPartSize();
+			int optionalParameterMatch = matchSize - curPathSegment.getMinParameters();
+			for (int matchSegment = 0; matchSegment < matchSize; matchSegment++)
+			{
+				if (pageParameters == null)
+				{
+					pageParameters = new PageParameters();
+				}
+
+				int curSegmentIndex = matchSegment + curPathSegment.getSegmentIndex();
+				String curSegment = mountSegments[curSegmentIndex];
+				String placeholder = getPlaceholder(curSegment);
+				String optionalPlaceholder = getOptionalPlaceholder(curSegment);
+				// extract the parameter from URL
+				if (placeholder != null)
+				{
+					pageParameters.add(placeholder,
+						url.getSegments().get(curSegmentIndex - skippedParameters));
+				}
+				else if (optionalPlaceholder != null && optionalParameterMatch > 0)
+				{
+					pageParameters.add(optionalPlaceholder,
+						url.getSegments().get(curSegmentIndex - skippedParameters));
+					optionalParameterMatch--;
+				}
+			}
+			skippedParameters += curPathSegment.getMaxParameters() - matchSize;
+		}
+		return pageParameters;
+	}
+
+	@Override
+	protected boolean urlStartsWith(Url url, String... segments)
+	{
+		if (url == null)
+		{
+			return false;
+		}
+		else
+		{
+			return getMatchedSegmentSizes(url) != null;
+		}
+	}
+
+	private int[] getMatchedSegmentSizes(Url url)
+	{
+		int[] ret = new int[pathSegments.size()];
+		int segmentIndex = 0;
+		int pathSegmentIndex = 0;
+		for (MountPathSegment curPathSegment : pathSegments.subList(0, pathSegments.size() - 1))
+		{
+			boolean foundFixedPart = false;
+			segmentIndex += curPathSegment.getMinParameters();
+			int max = Math.min(curPathSegment.getOptionalParameters() + 1,
+				url.getSegments().size() - segmentIndex);
+
+			for (int count = max - 1; count >= 0; count--)
+			{
+				if (url.getSegments()
+					.get(segmentIndex + count)
+					.equals(curPathSegment.getFixedPart()))
+				{
+					foundFixedPart = true;
+					segmentIndex += count + 1;
+					ret[pathSegmentIndex] = count + curPathSegment.getMinParameters() + 1;
+					break;
+				}
+			}
+			if (!foundFixedPart)
+				return null;
+			pathSegmentIndex++;
+		}
+		MountPathSegment lastSegment = pathSegments.get(pathSegments.size() - 1);
+		segmentIndex += lastSegment.getMinParameters();
+		if (segmentIndex > url.getSegments().size())
+			return null;
+		ret[pathSegmentIndex] = Math.min(lastSegment.getMaxParameters(), url.getSegments().size() -
+			segmentIndex + lastSegment.getMinParameters());
+		return ret;
+	}
+
+	protected PageParameters newPageParameters()
+	{
+		return new PageParameters();
+	}
+
+	@Override
+	public Url mapHandler(IRequestHandler requestHandler)
+	{
+		Url url = super.mapHandler(requestHandler);
+
+		if (url == null && requestHandler instanceof ListenerInterfaceRequestHandler &&
+			getRecreateMountedPagesAfterExpiry())
+		{
+			ListenerInterfaceRequestHandler handler = (ListenerInterfaceRequestHandler)requestHandler;
+			IRequestablePage page = handler.getPage();
+			if (checkPageInstance(page))
+			{
+				String componentPath = handler.getComponentPath();
+				RequestListenerInterface listenerInterface = handler.getListenerInterface();
+
+				Integer renderCount = null;
+				if (listenerInterface.isIncludeRenderCount())
+				{
+					renderCount = page.getRenderCount();
+				}
+
+				PageInfo pageInfo = new PageInfo(page.getPageId());
+				ComponentInfo componentInfo = new ComponentInfo(renderCount,
+					requestListenerInterfaceToString(listenerInterface), componentPath,
+					handler.getBehaviorIndex());
+				PageComponentInfo pageComponentInfo = new PageComponentInfo(pageInfo, componentInfo);
+				PageParameters parameters = new PageParameters(page.getPageParameters());
+				UrlInfo urlInfo = new UrlInfo(pageComponentInfo, page.getClass(),
+					parameters.mergeWith(handler.getPageParameters()));
+				url = buildUrl(urlInfo);
+			}
+		}
+
+		return url;
+	}
+
+	boolean getRecreateMountedPagesAfterExpiry()
+	{
+		return Application.get().getPageSettings().getRecreateMountedPagesAfterExpiry();
+	}
+
+	/**
+	 * @see AbstractBookmarkableMapper#buildUrl(AbstractBookmarkableMapper.UrlInfo)
+	 */
+	@Override
+	protected Url buildUrl(UrlInfo info)
+	{
+		Url url = new Url();
+		for (String s : mountSegments)
+		{
+			url.getSegments().add(s);
+		}
+		encodePageComponentInfo(url, info.getPageComponentInfo());
+
+		PageParameters copy = new PageParameters(info.getPageParameters());
+
+		int dropped = 0;
+		for (int i = 0; i < mountSegments.length; ++i)
+		{
+			String placeholder = getPlaceholder(mountSegments[i]);
+			String optionalPlaceholder = getOptionalPlaceholder(mountSegments[i]);
+			if (placeholder != null)
+			{
+				url.getSegments().set(i - dropped, copy.get(placeholder).toString(""));
+				copy.remove(placeholder);
+			}
+			else if (optionalPlaceholder != null)
+			{
+				if (copy.getNamedKeys().contains(optionalPlaceholder))
+				{
+					url.getSegments().set(i - dropped, copy.get(optionalPlaceholder).toString(""));
+					copy.remove(optionalPlaceholder);
+				}
+				else
+				{
+					url.getSegments().remove(i - dropped);
+					dropped++;
+				}
+			}
+		}
+
+		return encodePageParameters(url, copy, pageParametersEncoder);
+	}
+
+	/**
+	 * Check if the URL is for home page and the home page class match mounted class. If so,
+	 * redirect to mounted URL.
+	 * 
+	 * @param url
+	 * @return request handler or <code>null</code>
+	 */
+	private boolean checkHomePage(Url url)
+	{
+		if (url.getSegments().isEmpty() && url.getQueryParameters().isEmpty())
+		{
+			// this is home page
+			if (getPageClass().equals(getContext().getHomePageClass()) && redirectFromHomePage())
+			{
+				return true;
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * If this method returns <code>true</code> and application home page class is same as the class
+	 * mounted with this encoder, request to home page will result in a redirect to the mounted
+	 * path.
+	 * 
+	 * @return whether this encode should respond to home page request when home page class is same
+	 *         as mounted class.
+	 */
+	protected boolean redirectFromHomePage()
+	{
+		return true;
+	}
+
+	/**
+	 * @see AbstractBookmarkableMapper#pageMustHaveBeenCreatedBookmarkable()
+	 */
+	@Override
+	protected boolean pageMustHaveBeenCreatedBookmarkable()
+	{
+		return false;
+	}
+
+	/**
+	 * @see AbstractBookmarkableMapper#getCompatibilityScore(org.apache.wicket.request.Request)
+	 */
+	@Override
+	public int getCompatibilityScore(Request request)
+	{
+		if (urlStartsWith(request.getUrl(), mountSegments))
+		{
+			return mountSegments.length;
+		}
+		else
+		{
+			return 0;
+		}
+	}
+
+	/**
+	 * @see AbstractBookmarkableMapper#checkPageClass(java.lang.Class)
+	 */
+	@Override
+	protected boolean checkPageClass(Class<? extends IRequestablePage> pageClass)
+	{
+		return pageClass.equals(this.getPageClass());
+	}
+
+	private Class<? extends IRequestablePage> getPageClass()
+	{
+		return pageClassProvider.get();
+	}
+
+	@Override
+	public String toString()
+	{
+		return "MountedMapper [mountSegments=" + Strings.join("/", mountSegments) + "]";
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/53f07873/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/PackageMapper.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/PackageMapper.java b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/PackageMapper.java
new file mode 100644
index 0000000..949c244
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/PackageMapper.java
@@ -0,0 +1,231 @@
+/*
+ * 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.lang.reflect.Modifier;
+
+import org.apache.wicket.protocol.http.WebApplication;
+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;
+import org.apache.wicket.util.lang.PackageName;
+
+/**
+ * A request mapper that mounts all bookmarkable pages in a given package.
+ * <p>
+ * To mount this mapper onto a path use the {@link WebApplication#mountPackage(String, Class)}, ex:
+ * 
+ * <pre>
+ * MyApp#init() {
+ * 
+ *   super.init();
+ *   mountPackage(&quot;/my/path&quot;, MyPage.class);
+ * }
+ * </pre>
+ * 
+ * will result in urls like {@code /my/path/MyPage}
+ * </p>
+ * 
+ * <pre>
+ *  Page Class - Render (BookmarkablePageRequestHandler)
+ *  /MyPage
+ *  (will redirect to hybrid alternative if page is not stateless)
+ * 
+ *  Page Instance - Render Hybrid (RenderPageRequestHandler for pages that were created using bookmarkable URLs)
+ *  /MyPage?2
+ * 
+ *  Page Instance - Bookmarkable Listener (BookmarkableListenerInterfaceRequestHandler)
+ *  /MyPage?2-click-foo-bar-baz
+ *  /MyPage?2-click.1-foo-bar-baz (1 is behavior index)
+ *  (these will redirect to hybrid if page is not stateless)
+ * </pre>
+ */
+public class PackageMapper extends AbstractBookmarkableMapper
+{
+	/**
+	 * the name of the package for which all bookmarkable pages should be mounted
+	 */
+	private final PackageName packageName;
+
+	/** the encoder used to encode/decode the page parameters */
+	private final IPageParametersEncoder pageParametersEncoder;
+
+	/**
+	 * Construct.
+	 * 
+	 * @param packageName
+	 */
+	public PackageMapper(final PackageName packageName)
+	{
+		this(packageName, new PageParametersEncoder());
+	}
+
+	/**
+	 * Construct.
+	 * 
+	 * @param packageName
+	 * @param pageParametersEncoder
+	 */
+	public PackageMapper(final PackageName packageName,
+		final IPageParametersEncoder pageParametersEncoder)
+	{
+		Args.notNull(packageName, "packageName");
+		Args.notNull(pageParametersEncoder, "pageParametersEncoder");
+
+		this.packageName = packageName;
+		this.pageParametersEncoder = pageParametersEncoder;
+	}
+
+	/**
+	 * @see org.apache.wicket.request.mapper.AbstractBookmarkableMapper#buildUrl(org.apache.wicket.request.mapper.AbstractBookmarkableMapper.UrlInfo)
+	 */
+	@Override
+	protected Url buildUrl(UrlInfo info)
+	{
+		Class<? extends IRequestablePage> pageClass = info.getPageClass();
+		PackageName pageClassPackageName = PackageName.forClass(pageClass);
+		if (pageClassPackageName.equals(packageName))
+		{
+			Url url = new Url();
+
+			String fullyQualifiedClassName = pageClass.getName();
+			String packageRelativeClassName = fullyQualifiedClassName;
+			int packageNameLength = packageName.getName().length();
+			if (packageNameLength > 0)
+			{
+				packageRelativeClassName = fullyQualifiedClassName.substring(packageNameLength + 1);
+			}
+			packageRelativeClassName = transformForUrl(packageRelativeClassName);
+			url.getSegments().add(packageRelativeClassName);
+			encodePageComponentInfo(url, info.getPageComponentInfo());
+			return encodePageParameters(url, info.getPageParameters(), pageParametersEncoder);
+		}
+
+		return null;
+	}
+
+	/**
+	 * @see org.apache.wicket.request.mapper.AbstractBookmarkableMapper#parseRequest(org.apache.wicket.request.Request)
+	 */
+	@Override
+	protected UrlInfo parseRequest(Request request)
+	{
+		Url url = request.getUrl();
+		if (url.getSegments().size() >= 1)
+		{
+			// try to extract page and component information from URL
+			PageComponentInfo info = getPageComponentInfo(url);
+
+			// load the page class
+			String className = url.getSegments().get(0);
+			
+			if (isValidClassName(className) == false)
+			{
+				return null;
+			}
+
+			className = transformFromUrl(className);
+			String fullyQualifiedClassName = packageName.getName() + '.' + className;
+			Class<? extends IRequestablePage> pageClass = getPageClass(fullyQualifiedClassName);
+
+			if (pageClass != null && Modifier.isAbstract(pageClass.getModifiers()) == false &&
+				IRequestablePage.class.isAssignableFrom(pageClass))
+			{
+				// extract the PageParameters from URL if there are any
+				PageParameters pageParameters = extractPageParameters(request, 1,
+					pageParametersEncoder);
+
+				return new UrlInfo(info, pageClass, pageParameters);
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * filter out invalid class names for package mapper. getting trash for class names
+	 * can e.g. happen when the home page is in the same package that is mounted by package mapper
+	 * but the request was previously mapped by e.g. {@link HomePageMapper}. We then get some 
+	 * strange url like '/example/..' and wicket tries to look up class name '..'.
+	 * <p/>
+	 *  @see <a href="https://issues.apache.org/jira/browse/WICKET-4303">WICKET-4303</a>
+	 *  <p/>
+	 */
+	private boolean isValidClassName(String className)
+	{
+		// darn simple check - feel free to enhance this method to your needs
+		if (className == null)
+		{
+			return false;
+		}
+		// java class names never start with '.'
+		if (className.startsWith("."))
+		{
+			return false;
+		}
+		return true;
+	}
+
+	/**
+	 * Gives a chance to specializations of this mapper to transform the alias of the class name to
+	 * the real class name
+	 * 
+	 * @param classNameAlias
+	 *            the alias for the class name
+	 * @return the real class name
+	 */
+	protected String transformFromUrl(final String classNameAlias)
+	{
+		return classNameAlias;
+	}
+
+	/**
+	 * Gives a chance to specializations of this mapper to transform the real class name to an alias
+	 * which is prettier to represent in the Url
+	 * 
+	 * @param className
+	 *            the real class name
+	 * @return the class name alias
+	 */
+	protected String transformForUrl(final String className)
+	{
+		return className;
+	}
+
+	/**
+	 * @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)
+	{
+		// always return 0 here so that the mounts have higher priority
+		return 0;
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/53f07873/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/PageInstanceMapper.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/PageInstanceMapper.java b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/PageInstanceMapper.java
new file mode 100644
index 0000000..c133fc5
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/PageInstanceMapper.java
@@ -0,0 +1,168 @@
+/*
+ * 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.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.Request;
+import org.apache.wicket.request.Url;
+import org.apache.wicket.request.component.IRequestablePage;
+import org.apache.wicket.request.mapper.info.ComponentInfo;
+import org.apache.wicket.request.mapper.info.PageComponentInfo;
+import org.apache.wicket.request.mapper.info.PageInfo;
+
+/**
+ * Decodes and encodes the following URLs:
+ * 
+ * <pre>
+ *  Page Instance - Render (RenderPageRequestHandler)
+ *  /wicket/page?2
+ * 
+ * 
+ *  Page Instance - Listener (ListenerInterfaceRequestHandler)
+ *  /wicket/page?2-click-foo-bar-baz
+ *  /wicket/page?2-click.1-foo-bar-baz (1 is behavior index)
+ * </pre>
+ * 
+ * @author Matej Knopp
+ */
+public class PageInstanceMapper extends AbstractComponentMapper
+{
+	/**
+	 * Construct.
+	 */
+	public PageInstanceMapper()
+	{
+	}
+
+	/**
+	 * @see org.apache.wicket.request.IRequestMapper#mapRequest(org.apache.wicket.request.Request)
+	 */
+	@Override
+	public IRequestHandler mapRequest(Request request)
+	{
+		Url url = request.getUrl();
+		if (matches(url))
+		{
+			PageComponentInfo info = getPageComponentInfo(url);
+			if (info != null && info.getPageInfo().getPageId() != null)
+			{
+				Integer renderCount = info.getComponentInfo() != null ? info.getComponentInfo()
+					.getRenderCount() : null;
+
+				if (info.getComponentInfo() == null)
+				{
+					PageProvider provider = new PageProvider(info.getPageInfo().getPageId(),
+						renderCount);
+					provider.setPageSource(getContext());
+					// render page
+					return new RenderPageRequestHandler(provider);
+				}
+				else
+				{
+					ComponentInfo componentInfo = info.getComponentInfo();
+
+					PageAndComponentProvider provider = new PageAndComponentProvider(
+						info.getPageInfo().getPageId(), renderCount,
+						componentInfo.getComponentPath());
+
+					provider.setPageSource(getContext());
+
+					// listener interface
+					RequestListenerInterface listenerInterface = requestListenerInterfaceFromString(componentInfo.getListenerInterface());
+
+					return new ListenerInterfaceRequestHandler(provider, listenerInterface,
+						componentInfo.getBehaviorId());
+				}
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * @see org.apache.wicket.request.IRequestMapper#mapHandler(org.apache.wicket.request.IRequestHandler)
+	 */
+	@Override
+	public Url mapHandler(IRequestHandler requestHandler)
+	{
+		PageComponentInfo info = null;
+
+		if (requestHandler instanceof RenderPageRequestHandler)
+		{
+			IRequestablePage page = ((RenderPageRequestHandler)requestHandler).getPage();
+
+			PageInfo i = new PageInfo(page.getPageId());
+			info = new PageComponentInfo(i, null);
+		}
+		else if (requestHandler instanceof ListenerInterfaceRequestHandler)
+		{
+			ListenerInterfaceRequestHandler handler = (ListenerInterfaceRequestHandler)requestHandler;
+			IRequestablePage page = handler.getPage();
+			String componentPath = handler.getComponentPath();
+			RequestListenerInterface listenerInterface = handler.getListenerInterface();
+
+			Integer renderCount = null;
+			if (listenerInterface.isIncludeRenderCount())
+			{
+				renderCount = page.getRenderCount();
+			}
+
+			PageInfo pageInfo = new PageInfo(page.getPageId());
+			ComponentInfo componentInfo = new ComponentInfo(renderCount,
+				requestListenerInterfaceToString(listenerInterface), componentPath,
+				handler.getBehaviorIndex());
+			info = new PageComponentInfo(pageInfo, componentInfo);
+		}
+
+		if (info != null)
+		{
+			Url url = new Url();
+			url.getSegments().add(getContext().getNamespace());
+			url.getSegments().add(getContext().getPageIdentifier());
+			encodePageComponentInfo(url, info);
+			return url;
+		}
+		else
+		{
+			return null;
+		}
+	}
+
+	/**
+	 * @see org.apache.wicket.request.IRequestMapper#getCompatibilityScore(org.apache.wicket.request.Request)
+	 */
+	@Override
+	public int getCompatibilityScore(final 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 urlStartsWith(url, getContext().getNamespace(), getContext().getPageIdentifier());
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/53f07873/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/ResourceMapper.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/ResourceMapper.java b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/ResourceMapper.java
new file mode 100644
index 0000000..d1d58cd
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/ResourceMapper.java
@@ -0,0 +1,270 @@
+/*
+ * 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.request.handler.resource.ResourceReferenceRequestHandler;
+import org.apache.wicket.request.mapper.AbstractMapper;
+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.request.resource.IResource;
+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.lang.Args;
+import org.apache.wicket.util.string.Strings;
+
+/**
+ * A {@link IRequestMapper} to mount resources to a custom mount path
+ * <ul>
+ * <li>maps indexed parameters to path segments</li>
+ * <li>maps named parameters to query string arguments or placeholder path segments</li>
+ * </ul>
+ * 
+ * <strong>sample structure of url</strong>
+ * 
+ * <pre>
+ *    /myresources/${category}/images/[indexed-param-0]/[indexed-param-1]?[named-param-1=value]&[named-param-2=value2]
+ * </pre>
+ * 
+ * <h4>sample usage</h4>
+ * 
+ * in your wicket application's init() method use a statement like this
+ * <p/>
+ * 
+ * <pre>
+ * mountResource(&quot;/images&quot;, new ImagesResourceReference()));
+ * </pre>
+ * 
+ * Note: Mounted this way the resource reference has application scope, i.e. it is shared between
+ * all users of the application. It is recommended to not keep any state in it.
+ * 
+ * @see org.apache.wicket.protocol.http.WebApplication#mountResource(String,
+ *      org.apache.wicket.request.resource.ResourceReference)
+ * 
+ * @author Peter Ertl
+ */
+public class ResourceMapper extends AbstractMapper implements IRequestMapper
+{
+	// encode page parameters into url + decode page parameters from url
+	private final IPageParametersEncoder parametersEncoder;
+
+	// mount path (= segments) the resource is bound to
+	private final String[] mountSegments;
+
+	// resource that the mapper links to
+	private final ResourceReference resourceReference;
+
+	/**
+	 * create a resource mapper for a resource
+	 * 
+	 * @param path
+	 *            mount path for the resource
+	 * @param resourceReference
+	 *            resource reference that should be linked to the mount path
+	 * 
+	 * @see #ResourceMapper(String, org.apache.wicket.request.resource.ResourceReference,
+	 *      org.apache.wicket.request.mapper.parameter.IPageParametersEncoder)
+	 */
+	public ResourceMapper(String path, ResourceReference resourceReference)
+	{
+		this(path, resourceReference, new PageParametersEncoder());
+	}
+
+	/**
+	 * create a resource mapper for a resource
+	 * 
+	 * @param path
+	 *            mount path for the resource
+	 * @param resourceReference
+	 *            resource reference that should be linked to the mount path
+	 * @param encoder
+	 *            encoder for url parameters
+	 */
+	public ResourceMapper(String path, ResourceReference resourceReference,
+		IPageParametersEncoder encoder)
+	{
+		Args.notEmpty(path, "path");
+		Args.notNull(resourceReference, "resourceReference");
+		Args.notNull(encoder, "encoder");
+
+		this.resourceReference = resourceReference;
+		mountSegments = getMountSegments(path);
+		parametersEncoder = encoder;
+	}
+
+	@Override
+	public IRequestHandler mapRequest(final Request request)
+	{
+		final Url url = new Url(request.getUrl());
+
+		// now extract the page parameters from the request url
+		PageParameters parameters = extractPageParameters(request, mountSegments.length,
+			parametersEncoder);
+
+		// remove caching information from current request
+		removeCachingDecoration(url, parameters);
+
+		// check if url matches mount path
+		if (urlStartsWith(url, mountSegments) == false)
+		{
+			return null;
+		}
+
+		// check if there are placeholders in mount segments
+		for (int index = 0; index < mountSegments.length; ++index)
+		{
+			String placeholder = getPlaceholder(mountSegments[index]);
+
+			if (placeholder != null)
+			{
+				// extract the parameter from URL
+				if (parameters == null)
+				{
+					parameters = new PageParameters();
+				}
+				parameters.add(placeholder, url.getSegments().get(index));
+			}
+		}
+		return new ResourceReferenceRequestHandler(resourceReference, parameters);
+	}
+
+	@Override
+	public int getCompatibilityScore(Request request)
+	{
+		return 0; // pages always have priority over resources
+	}
+
+	@Override
+	public Url mapHandler(IRequestHandler requestHandler)
+	{
+		if ((requestHandler instanceof ResourceReferenceRequestHandler) == false)
+		{
+			return null;
+		}
+
+		ResourceReferenceRequestHandler handler = (ResourceReferenceRequestHandler)requestHandler;
+
+		// see if request handler addresses the resource reference we serve
+		if (resourceReference.equals(handler.getResourceReference()) == false)
+		{
+			return null;
+		}
+
+		Url url = new Url();
+
+		// add mount path segments
+		for (String segment : mountSegments)
+		{
+			url.getSegments().add(segment);
+		}
+
+		// replace placeholder parameters
+		PageParameters parameters = new PageParameters(handler.getPageParameters());
+
+		for (int index = 0; index < mountSegments.length; ++index)
+		{
+			String placeholder = getPlaceholder(mountSegments[index]);
+
+			if (placeholder != null)
+			{
+				url.getSegments().set(index, parameters.get(placeholder).toString(""));
+				parameters.remove(placeholder);
+			}
+		}
+
+		// add caching information
+		addCachingDecoration(url, parameters);
+
+		// create url
+		return encodePageParameters(url, parameters, parametersEncoder);
+	}
+
+	protected IResourceCachingStrategy getCachingStrategy()
+	{
+		return Application.get().getResourceSettings().getCachingStrategy();
+	}
+
+	protected void addCachingDecoration(Url url, PageParameters parameters)
+	{
+		final List<String> segments = url.getSegments();
+		final int lastSegmentAt = segments.size() - 1;
+		final String filename = segments.get(lastSegmentAt);
+
+		if (Strings.isEmpty(filename) == false)
+		{
+			// TODO is calling getResource() a potential performance bottleneck?
+			final IResource resource = resourceReference.getResource();
+
+			if (resource instanceof IStaticCacheableResource)
+			{
+				final IStaticCacheableResource cacheable = (IStaticCacheableResource)resource;
+				final ResourceUrl cacheUrl = new ResourceUrl(filename, parameters);
+
+				getCachingStrategy().decorateUrl(cacheUrl, cacheable);
+
+				if (Strings.isEmpty(cacheUrl.getFileName()))
+				{
+					throw new IllegalStateException("caching strategy returned empty name for " +
+						resource);
+				}
+				segments.set(lastSegmentAt, cacheUrl.getFileName());
+			}
+		}
+	}
+
+	protected void removeCachingDecoration(Url url, PageParameters parameters)
+	{
+		final List<String> segments = url.getSegments();
+
+		if (segments.isEmpty() == false)
+		{
+			// get filename (the last segment)
+			final int lastSegmentAt = segments.size() - 1;
+			String filename = segments.get(lastSegmentAt);
+
+			// ignore requests with empty filename
+			if (Strings.isEmpty(filename))
+			{
+				return;
+			}
+
+			// create resource url from filename and query parameters
+			final ResourceUrl resourceUrl = new ResourceUrl(filename, parameters);
+
+			// remove caching information from request
+			getCachingStrategy().undecorateUrl(resourceUrl);
+
+			// check for broken caching strategy (this must never happen)
+			if (Strings.isEmpty(resourceUrl.getFileName()))
+			{
+				throw new IllegalStateException("caching strategy returned empty name for " +
+					resourceUrl);
+			}
+
+			segments.set(lastSegmentAt, resourceUrl.getFileName());
+		}
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/53f07873/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/ResourceReferenceMapper.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/ResourceReferenceMapper.java b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/ResourceReferenceMapper.java
new file mode 100644
index 0000000..ce69bb2
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/ResourceReferenceMapper.java
@@ -0,0 +1,56 @@
+/*
+ * 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.request.mapper.ParentPathReferenceRewriter;
+import org.apache.wicket.request.mapper.parameter.IPageParametersEncoder;
+import org.apache.wicket.request.resource.ResourceReference;
+import org.apache.wicket.request.resource.caching.IResourceCachingStrategy;
+import org.apache.wicket.util.IProvider;
+
+/**
+ * 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 igor.vaynberg
+ */
+public class ResourceReferenceMapper extends ParentPathReferenceRewriter
+{
+	/**
+	 * Construct.
+	 * 
+	 * @param pageParametersEncoder
+	 * @param parentPathPartEscapeSequence
+	 * @param cachingStrategy
+	 */
+	public ResourceReferenceMapper(IPageParametersEncoder pageParametersEncoder,
+		IProvider<String> parentPathPartEscapeSequence,
+		IProvider<IResourceCachingStrategy> cachingStrategy)
+	{
+		super(new BasicResourceReferenceMapper(pageParametersEncoder, cachingStrategy),
+			parentPathPartEscapeSequence);
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/53f07873/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/StalePageException.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/StalePageException.java b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/StalePageException.java
new file mode 100644
index 0000000..a3af0e0
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/StalePageException.java
@@ -0,0 +1,53 @@
+/*
+ * 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.WicketRuntimeException;
+import org.apache.wicket.request.component.IRequestablePage;
+
+/**
+ * Exception invoked when when stale link has been clicked. The page should then be rerendered with
+ * an explanatory error message.
+ * 
+ * @author Matej Knopp
+ */
+public class StalePageException extends WicketRuntimeException
+{
+	private static final long serialVersionUID = 1L;
+
+	private final transient IRequestablePage page;
+
+	/**
+	 * 
+	 * Construct.
+	 * 
+	 * @param page
+	 */
+	public StalePageException(IRequestablePage page)
+	{
+		this.page = page;
+	}
+
+	/**
+	 * 
+	 * @return page instance
+	 */
+	public IRequestablePage getPage()
+	{
+		return page;
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/53f07873/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/KeyInSessionSunJceCryptFactory.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/KeyInSessionSunJceCryptFactory.java b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/KeyInSessionSunJceCryptFactory.java
new file mode 100644
index 0000000..7fe43ca
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/KeyInSessionSunJceCryptFactory.java
@@ -0,0 +1,65 @@
+/*
+ * 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.util.crypt;
+
+import java.util.UUID;
+
+import org.apache.wicket.MetaDataKey;
+import org.apache.wicket.Session;
+import org.apache.wicket.util.crypt.ICrypt;
+import org.apache.wicket.util.crypt.ICryptFactory;
+import org.apache.wicket.util.crypt.SunJceCrypt;
+
+/**
+ * Crypt factory that produces {@link SunJceCrypt} instances based on http session-specific
+ * encryption key. This allows each user to have their own encryption key, hardening against CSRF
+ * attacks.
+ * 
+ * Note that the use of this crypt factory will result in an immediate creation of a http session
+ * 
+ * @author igor.vaynberg
+ */
+public class KeyInSessionSunJceCryptFactory implements ICryptFactory
+{
+	/** metadata-key used to store crypto-key in session metadata */
+	private static MetaDataKey<String> KEY = new MetaDataKey<String>()
+	{
+		private static final long serialVersionUID = 1L;
+	};
+
+
+	@Override
+	public ICrypt newCrypt()
+	{
+		Session session = Session.get();
+		session.bind();
+
+		// retrieve or generate encryption key from session
+		String key = session.getMetaData(KEY);
+		if (key == null)
+		{
+			// generate new key
+			key = session.getId() + "." + UUID.randomUUID().toString();
+			session.setMetaData(KEY, key);
+		}
+
+		// build the crypt based on session key
+		ICrypt crypt = new SunJceCrypt();
+		crypt.setKey(key);
+		return crypt;
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/53f07873/wicket-core/src/main/java/org/apache/wicket/core/util/file/WebApplicationPath.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/file/WebApplicationPath.java b/wicket-core/src/main/java/org/apache/wicket/core/util/file/WebApplicationPath.java
new file mode 100644
index 0000000..b29832b
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/file/WebApplicationPath.java
@@ -0,0 +1,144 @@
+/*
+ * 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.util.file;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.servlet.ServletContext;
+
+import org.apache.wicket.util.file.Folder;
+import org.apache.wicket.util.file.Path;
+import org.apache.wicket.util.resource.IResourceStream;
+import org.apache.wicket.core.util.resource.UrlResourceStream;
+import org.apache.wicket.util.string.StringList;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * Maintain a list of paths which might either be ordinary folders of the filesystem or relative
+ * paths to the web application's servlet context.
+ * 
+ * @author Johan Compagner
+ */
+public final class WebApplicationPath extends Path
+{
+	private final static Logger log = LoggerFactory.getLogger(WebApplicationPath.class);
+
+	private static final String WEB_INF = "WEB-INF/";
+
+	/** The list of urls in the path */
+	private final List<String> webappPaths = new ArrayList<String>();
+
+	/** The web apps servlet context */
+	private final ServletContext servletContext;
+
+	/**
+	 * Constructor
+	 * 
+	 * @param servletContext
+	 *            The webapplication context where the resources must be loaded from
+	 */
+	public WebApplicationPath(final ServletContext servletContext)
+	{
+		this.servletContext = servletContext;
+
+		// adding root so servlet context resources are always checked
+		webappPaths.add("/");
+	}
+
+	/**
+	 * @param path
+	 *            add a path that is lookup through the servlet context
+	 */
+	@Override
+	public void add(String path)
+	{
+		final Folder folder = new Folder(path);
+		if (folder.exists())
+		{
+			log.debug("Added path '{}' as a folder.", path);
+			super.add(folder);
+		}
+		else
+		{
+			if (!path.startsWith("/"))
+			{
+				path = "/" + path;
+			}
+			if (!path.endsWith("/"))
+			{
+				path += "/";
+			}
+			log.debug("Added path '{}' as a web path.", path);
+			webappPaths.add(path);
+		}
+	}
+
+	/**
+	 * 
+	 * @see org.apache.wicket.util.file.IResourceFinder#find(Class, String)
+	 */
+	@Override
+	public IResourceStream find(final Class<?> clazz, final String pathname)
+	{
+		if (pathname == null)
+		{
+			return null;
+		}
+
+		IResourceStream resourceStream = super.find(clazz, pathname);
+
+		if (resourceStream == null && pathname.startsWith(WEB_INF) == false)
+		{
+			for (String path : webappPaths)
+			{
+				try
+				{
+					final URL url = servletContext.getResource(path + pathname);
+					if (url != null)
+					{
+						resourceStream = new UrlResourceStream(url);
+						break;
+					}
+				}
+				catch (Exception ex)
+				{
+					// ignore, file couldn't be found
+				}
+			}
+		}
+
+		return resourceStream;
+	}
+
+	public List<String> getWebappPaths()
+	{
+		return webappPaths;
+	}
+	/**
+	 * @see java.lang.Object#toString()
+	 */
+	@Override
+	public String toString()
+	{
+		return "[folders = " + StringList.valueOf(getFolders()) + ", webapppaths: " +
+			StringList.valueOf(webappPaths) + "]";
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/53f07873/wicket-core/src/main/java/org/apache/wicket/core/util/io/SerializableChecker.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/io/SerializableChecker.java b/wicket-core/src/main/java/org/apache/wicket/core/util/io/SerializableChecker.java
new file mode 100644
index 0000000..9cc9829
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/io/SerializableChecker.java
@@ -0,0 +1,734 @@
+/*
+ * 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.util.io;
+
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.NotSerializableException;
+import java.io.ObjectOutput;
+import java.io.ObjectOutputStream;
+import java.io.ObjectStreamClass;
+import java.io.ObjectStreamField;
+import java.io.OutputStream;
+import java.io.Serializable;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Set;
+import java.util.Stack;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.WicketRuntimeException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * Utility class that analyzes objects for non-serializable nodes. Construct, then call
+ * {@link #check(Object)} with the object you want to check. When a non-serializable object is
+ * found, a {@link WicketNotSerializableException} is thrown with a message that shows the trace up
+ * to the not-serializable object. The exception is thrown for the first non-serializable instance
+ * it encounters, so multiple problems will not be shown.
+ * <p>
+ * As this class depends heavily on JDK's serialization internals using introspection, analyzing may
+ * not be possible, for instance when the runtime environment does not have sufficient rights to set
+ * fields accessible that would otherwise be hidden. You should call
+ * {@link SerializableChecker#isAvailable()} to see whether this class can operate properly. If it
+ * doesn't, you should fall back to e.g. re-throwing/ printing the {@link NotSerializableException}
+ * you probably got before using this class.
+ * </p>
+ * 
+ * @author eelcohillenius
+ * @author Al Maw
+ */
+public final class SerializableChecker extends ObjectOutputStream
+{
+
+	/** log. */
+	private static final Logger log = LoggerFactory.getLogger(SerializableChecker.class);
+
+	/**
+	 * Exception that is thrown when a non-serializable object was found.
+	 */
+	public static final class WicketNotSerializableException extends WicketRuntimeException
+	{
+		private static final long serialVersionUID = 1L;
+
+		WicketNotSerializableException(String message, Throwable cause)
+		{
+			super(message, cause);
+		}
+	}
+
+	/**
+	 * Does absolutely nothing.
+	 */
+	private static class NoopOutputStream extends OutputStream
+	{
+		@Override
+		public void close()
+		{
+		}
+
+		@Override
+		public void flush()
+		{
+		}
+
+		@Override
+		public void write(byte[] b)
+		{
+		}
+
+		@Override
+		public void write(byte[] b, int i, int l)
+		{
+		}
+
+		@Override
+		public void write(int b)
+		{
+		}
+	}
+
+	private static abstract class ObjectOutputAdaptor implements ObjectOutput
+	{
+
+		@Override
+		public void close() throws IOException
+		{
+		}
+
+		@Override
+		public void flush() throws IOException
+		{
+		}
+
+		@Override
+		public void write(byte[] b) throws IOException
+		{
+		}
+
+		@Override
+		public void write(byte[] b, int off, int len) throws IOException
+		{
+		}
+
+		@Override
+		public void write(int b) throws IOException
+		{
+		}
+
+		@Override
+		public void writeBoolean(boolean v) throws IOException
+		{
+		}
+
+		@Override
+		public void writeByte(int v) throws IOException
+		{
+		}
+
+		@Override
+		public void writeBytes(String s) throws IOException
+		{
+		}
+
+		@Override
+		public void writeChar(int v) throws IOException
+		{
+		}
+
+		@Override
+		public void writeChars(String s) throws IOException
+		{
+		}
+
+		@Override
+		public void writeDouble(double v) throws IOException
+		{
+		}
+
+		@Override
+		public void writeFloat(float v) throws IOException
+		{
+		}
+
+		@Override
+		public void writeInt(int v) throws IOException
+		{
+		}
+
+		@Override
+		public void writeLong(long v) throws IOException
+		{
+		}
+
+		@Override
+		public void writeShort(int v) throws IOException
+		{
+		}
+
+		@Override
+		public void writeUTF(String str) throws IOException
+		{
+		}
+	}
+
+	/** Holds information about the field and the resulting object being traced. */
+	private static final class TraceSlot
+	{
+		private final String fieldDescription;
+
+		private final Object object;
+
+		TraceSlot(Object object, String fieldDescription)
+		{
+			super();
+			this.object = object;
+			this.fieldDescription = fieldDescription;
+		}
+
+		@Override
+		public String toString()
+		{
+			return object.getClass() + " - " + fieldDescription;
+		}
+	}
+
+	private static final NoopOutputStream DUMMY_OUTPUT_STREAM = new NoopOutputStream();
+
+	/** Whether we can execute the tests. If false, check will just return. */
+	private static boolean available = true;
+
+	// this hack - accessing the serialization API through introspection - is
+	// the only way to use Java serialization for our purposes without writing
+	// the whole thing from scratch (and even then, it would be limited). This
+	// way of working is of course fragile for internal API changes, but as we
+	// do an extra check on availability and we report when we can't use this
+	// introspection fu, we'll find out soon enough and clients on this class
+	// can fall back on Java's default exception for serialization errors (which
+	// sucks and is the main reason for this attempt).
+	private static Method LOOKUP_METHOD;
+
+	private static Method GET_CLASS_DATA_LAYOUT_METHOD;
+
+	private static Method GET_NUM_OBJ_FIELDS_METHOD;
+
+	private static Method GET_OBJ_FIELD_VALUES_METHOD;
+
+	private static Method GET_FIELD_METHOD;
+
+	private static Method HAS_WRITE_REPLACE_METHOD_METHOD;
+
+	private static Method INVOKE_WRITE_REPLACE_METHOD;
+
+	static
+	{
+		try
+		{
+			LOOKUP_METHOD = ObjectStreamClass.class.getDeclaredMethod("lookup", new Class[] {
+					Class.class, Boolean.TYPE });
+			LOOKUP_METHOD.setAccessible(true);
+
+			GET_CLASS_DATA_LAYOUT_METHOD = ObjectStreamClass.class.getDeclaredMethod(
+				"getClassDataLayout", (Class[])null);
+			GET_CLASS_DATA_LAYOUT_METHOD.setAccessible(true);
+
+			GET_NUM_OBJ_FIELDS_METHOD = ObjectStreamClass.class.getDeclaredMethod(
+				"getNumObjFields", (Class[])null);
+			GET_NUM_OBJ_FIELDS_METHOD.setAccessible(true);
+
+			GET_OBJ_FIELD_VALUES_METHOD = ObjectStreamClass.class.getDeclaredMethod(
+				"getObjFieldValues", new Class[] { Object.class, Object[].class });
+			GET_OBJ_FIELD_VALUES_METHOD.setAccessible(true);
+
+			GET_FIELD_METHOD = ObjectStreamField.class.getDeclaredMethod("getField", (Class[])null);
+			GET_FIELD_METHOD.setAccessible(true);
+
+			HAS_WRITE_REPLACE_METHOD_METHOD = ObjectStreamClass.class.getDeclaredMethod(
+				"hasWriteReplaceMethod", (Class[])null);
+			HAS_WRITE_REPLACE_METHOD_METHOD.setAccessible(true);
+
+			INVOKE_WRITE_REPLACE_METHOD = ObjectStreamClass.class.getDeclaredMethod(
+				"invokeWriteReplace", new Class[] { Object.class });
+			INVOKE_WRITE_REPLACE_METHOD.setAccessible(true);
+		}
+		catch (Exception e)
+		{
+			log.warn("SerializableChecker not available", e);
+			available = false;
+		}
+	}
+
+	/**
+	 * Gets whether we can execute the tests. If false, calling {@link #check(Object)} will just
+	 * return and you are advised to rely on the {@link NotSerializableException}. Clients are
+	 * advised to call this method prior to calling the check method.
+	 * 
+	 * @return whether security settings and underlying API etc allow for accessing the
+	 *         serialization API using introspection
+	 */
+	public static boolean isAvailable()
+	{
+		return available;
+	}
+
+	/** object stack that with the trace path. */
+	private final LinkedList<TraceSlot> traceStack = new LinkedList<TraceSlot>();
+
+	/** set for checking circular references. */
+	private final Map<Object, Object> checked = new IdentityHashMap<Object, Object>();
+
+	/** string stack with current names pushed. */
+	private final LinkedList<String> nameStack = new LinkedList<String>();
+
+	/** root object being analyzed. */
+	private Object root;
+
+	/** set of classes that had no writeObject methods at lookup (to avoid repeated checking) */
+	private final Set<Class<?>> writeObjectMethodMissing = new HashSet<Class<?>>();
+
+	/** current simple field name. */
+	private String simpleName = "";
+
+	/** current full field description. */
+	private String fieldDescription;
+
+	/** Exception that should be set as the cause when throwing a new exception. */
+	private final NotSerializableException exception;
+
+	private final Stack<Object> stack = new Stack<Object>();
+
+	/**
+	 * Construct.
+	 * 
+	 * @param exception
+	 *            exception that should be set as the cause when throwing a new exception
+	 * 
+	 * @throws IOException
+	 */
+	public SerializableChecker(NotSerializableException exception) throws IOException
+	{
+		this.exception = exception;
+	}
+
+	/**
+	 * @see java.io.ObjectOutputStream#reset()
+	 */
+	@Override
+	public void reset() throws IOException
+	{
+		root = null;
+		checked.clear();
+		fieldDescription = null;
+		simpleName = null;
+		traceStack.clear();
+		nameStack.clear();
+		writeObjectMethodMissing.clear();
+	}
+
+	private void check(Object obj)
+	{
+		if (obj == null)
+		{
+			return;
+		}
+
+		try
+		{
+			if (stack.contains(obj))
+			{
+				return;
+			}
+		}
+		catch (RuntimeException e)
+		{
+			log.warn("Wasn't possible to check the object " + obj.getClass() +
+				" possible due an problematic implementation of equals method");
+			/*
+			 * Can't check if this obj were in stack, giving up because we don't want to throw an
+			 * invaluable exception to user. The main goal of this checker is to find non
+			 * serializable data
+			 */
+			return;
+		}
+
+		stack.push(obj);
+		try
+		{
+			internalCheck(obj);
+		}
+		finally
+		{
+			stack.pop();
+		}
+	}
+
+	private void internalCheck(Object obj)
+	{
+		if (obj == null)
+		{
+			return;
+		}
+
+		Class<?> cls = obj.getClass();
+		nameStack.add(simpleName);
+		traceStack.add(new TraceSlot(obj, fieldDescription));
+
+		if (!(obj instanceof Serializable) && (!Proxy.isProxyClass(cls)))
+		{
+			throw new WicketNotSerializableException(
+				toPrettyPrintedStack(obj.getClass().getName()), exception);
+		}
+
+		ObjectStreamClass desc;
+		for (;;)
+		{
+			try
+			{
+				desc = (ObjectStreamClass)LOOKUP_METHOD.invoke(null, cls, Boolean.TRUE);
+				Class<?> repCl;
+				if (!(Boolean)HAS_WRITE_REPLACE_METHOD_METHOD.invoke(desc, (Object[])null) ||
+					(obj = INVOKE_WRITE_REPLACE_METHOD.invoke(desc, obj)) == null ||
+					(repCl = obj.getClass()) == cls)
+				{
+					break;
+				}
+				cls = repCl;
+			}
+			catch (IllegalAccessException e)
+			{
+				throw new RuntimeException(e);
+			}
+			catch (InvocationTargetException e)
+			{
+				throw new RuntimeException(e);
+			}
+		}
+
+		if (cls.isPrimitive())
+		{
+			// skip
+		}
+		else if (cls.isArray())
+		{
+			checked.put(obj, null);
+			Class<?> ccl = cls.getComponentType();
+			if (!(ccl.isPrimitive()))
+			{
+				Object[] objs = (Object[])obj;
+				for (int i = 0; i < objs.length; i++)
+				{
+					String arrayPos = "[" + i + "]";
+					simpleName = arrayPos;
+					fieldDescription += arrayPos;
+					check(objs[i]);
+				}
+			}
+		}
+		else if (obj instanceof Externalizable && (!Proxy.isProxyClass(cls)))
+		{
+			Externalizable extObj = (Externalizable)obj;
+			try
+			{
+				extObj.writeExternal(new ObjectOutputAdaptor()
+				{
+					private int count = 0;
+
+					@Override
+					public void writeObject(Object streamObj) throws IOException
+					{
+						// Check for circular reference.
+						if (checked.containsKey(streamObj))
+						{
+							return;
+						}
+
+						checked.put(streamObj, null);
+						String arrayPos = "[write:" + count++ + "]";
+						simpleName = arrayPos;
+						fieldDescription += arrayPos;
+
+						check(streamObj);
+					}
+				});
+			}
+			catch (Exception e)
+			{
+				if (e instanceof WicketNotSerializableException)
+				{
+					throw (WicketNotSerializableException)e;
+				}
+				log.warn("error delegating to Externalizable : " + e.getMessage() + ", path: " +
+					currentPath());
+			}
+		}
+		else
+		{
+			Method writeObjectMethod = null;
+			if (writeObjectMethodMissing.contains(cls) == false)
+			{
+				try
+				{
+					writeObjectMethod = cls.getDeclaredMethod("writeObject",
+						new Class[] { java.io.ObjectOutputStream.class });
+				}
+				catch (SecurityException e)
+				{
+					// we can't access / set accessible to true
+					writeObjectMethodMissing.add(cls);
+				}
+				catch (NoSuchMethodException e)
+				{
+					// cls doesn't have that method
+					writeObjectMethodMissing.add(cls);
+				}
+			}
+
+			final Object original = obj;
+			if (writeObjectMethod != null)
+			{
+				class InterceptingObjectOutputStream extends ObjectOutputStream
+				{
+					private int counter;
+
+					InterceptingObjectOutputStream() throws IOException
+					{
+						super(DUMMY_OUTPUT_STREAM);
+						enableReplaceObject(true);
+					}
+
+					@Override
+					protected Object replaceObject(Object streamObj) throws IOException
+					{
+						if (streamObj == original)
+						{
+							return streamObj;
+						}
+
+						counter++;
+						// Check for circular reference.
+						if (checked.containsKey(streamObj))
+						{
+							return null;
+						}
+
+						checked.put(streamObj, null);
+						String arrayPos = "[write:" + counter + "]";
+						simpleName = arrayPos;
+						fieldDescription += arrayPos;
+						check(streamObj);
+						return streamObj;
+					}
+				}
+				try
+				{
+					InterceptingObjectOutputStream ioos = new InterceptingObjectOutputStream();
+					ioos.writeObject(obj);
+				}
+				catch (Exception e)
+				{
+					if (e instanceof WicketNotSerializableException)
+					{
+						throw (WicketNotSerializableException)e;
+					}
+					log.warn("error delegating to writeObject : " + e.getMessage() + ", path: " +
+						currentPath());
+				}
+			}
+			else
+			{
+				Object[] slots;
+				try
+				{
+					slots = (Object[])GET_CLASS_DATA_LAYOUT_METHOD.invoke(desc, (Object[])null);
+				}
+				catch (Exception e)
+				{
+					throw new RuntimeException(e);
+				}
+				for (Object slot : slots)
+				{
+					ObjectStreamClass slotDesc;
+					try
+					{
+						Field descField = slot.getClass().getDeclaredField("desc");
+						descField.setAccessible(true);
+						slotDesc = (ObjectStreamClass)descField.get(slot);
+					}
+					catch (Exception e)
+					{
+						throw new RuntimeException(e);
+					}
+					checked.put(obj, null);
+					checkFields(obj, slotDesc);
+				}
+			}
+		}
+
+		traceStack.removeLast();
+		nameStack.removeLast();
+	}
+
+	private void checkFields(Object obj, ObjectStreamClass desc)
+	{
+		int numFields;
+		try
+		{
+			numFields = (Integer)GET_NUM_OBJ_FIELDS_METHOD.invoke(desc, (Object[])null);
+		}
+		catch (IllegalAccessException e)
+		{
+			throw new RuntimeException(e);
+		}
+		catch (InvocationTargetException e)
+		{
+			throw new RuntimeException(e);
+		}
+
+		if (numFields > 0)
+		{
+			int numPrimFields;
+			ObjectStreamField[] fields = desc.getFields();
+			Object[] objVals = new Object[numFields];
+			numPrimFields = fields.length - objVals.length;
+			try
+			{
+				GET_OBJ_FIELD_VALUES_METHOD.invoke(desc, obj, objVals);
+			}
+			catch (IllegalAccessException e)
+			{
+				throw new RuntimeException(e);
+			}
+			catch (InvocationTargetException e)
+			{
+				throw new RuntimeException(e);
+			}
+			for (int i = 0; i < objVals.length; i++)
+			{
+				if (objVals[i] instanceof String || objVals[i] instanceof Number ||
+					objVals[i] instanceof Date || objVals[i] instanceof Boolean ||
+					objVals[i] instanceof Class)
+				{
+					// filter out common cases
+					continue;
+				}
+
+				// Check for circular reference.
+				if (checked.containsKey(objVals[i]))
+				{
+					continue;
+				}
+
+				ObjectStreamField fieldDesc = fields[numPrimFields + i];
+				Field field;
+				try
+				{
+					field = (Field)GET_FIELD_METHOD.invoke(fieldDesc, (Object[])null);
+				}
+				catch (IllegalAccessException e)
+				{
+					throw new RuntimeException(e);
+				}
+				catch (InvocationTargetException e)
+				{
+					throw new RuntimeException(e);
+				}
+
+				field.getName();
+				simpleName = field.getName();
+				fieldDescription = field.toString();
+				check(objVals[i]);
+			}
+		}
+	}
+
+	/**
+	 * @return name from root to current node concatenated with slashes
+	 */
+	private StringBuilder currentPath()
+	{
+		StringBuilder b = new StringBuilder();
+		for (Iterator<String> it = nameStack.iterator(); it.hasNext();)
+		{
+			b.append(it.next());
+			if (it.hasNext())
+			{
+				b.append('/');
+			}
+		}
+		return b;
+	}
+
+	/**
+	 * Dump with indentation.
+	 * 
+	 * @param type
+	 *            the type that couldn't be serialized
+	 * @return A very pretty dump
+	 */
+	private final String toPrettyPrintedStack(String type)
+	{
+		StringBuilder result = new StringBuilder();
+		StringBuilder spaces = new StringBuilder();
+		result.append("Unable to serialize class: ");
+		result.append(type);
+		result.append("\nField hierarchy is:");
+		for (Iterator<TraceSlot> i = traceStack.listIterator(); i.hasNext();)
+		{
+			spaces.append("  ");
+			TraceSlot slot = i.next();
+			result.append("\n").append(spaces).append(slot.fieldDescription);
+			result.append(" [class=").append(slot.object.getClass().getName());
+			if (slot.object instanceof Component)
+			{
+				Component component = (Component)slot.object;
+				result.append(", path=").append(component.getPath());
+			}
+			result.append("]");
+		}
+		result.append(" <----- field that is not serializable");
+		return result.toString();
+	}
+
+	/**
+	 * @see java.io.ObjectOutputStream#writeObjectOverride(java.lang.Object)
+	 */
+	@Override
+	protected final void writeObjectOverride(Object obj) throws IOException
+	{
+		if (!available)
+		{
+			return;
+		}
+		root = obj;
+		if (fieldDescription == null)
+		{
+			fieldDescription = (root instanceof Component) ? ((Component)root).getPath() : "";
+		}
+
+		check(root);
+	}
+}
\ No newline at end of file