You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@wicket.apache.org by sv...@apache.org on 2018/10/30 12:59:29 UTC

[50/52] [abbrv] wicket git commit: WICKET-6563 new IPageStore implementation

http://git-wip-us.apache.org/repos/asf/wicket/blob/c43d3a33/wicket-core/src/main/java/org/apache/wicket/pageStore/AsynchronousDataStore.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/pageStore/AsynchronousDataStore.java b/wicket-core/src/main/java/org/apache/wicket/pageStore/AsynchronousDataStore.java
deleted file mode 100644
index f749354..0000000
--- a/wicket-core/src/main/java/org/apache/wicket/pageStore/AsynchronousDataStore.java
+++ /dev/null
@@ -1,344 +0,0 @@
-/*
- * 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.pageStore;
-
-import java.util.Iterator;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.wicket.util.lang.Args;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Facade for {@link IDataStore} that does the actual saving in worker thread.
- * <p>
- * Creates an {@link Entry} for each triple (sessionId, pageId, data) and puts it in
- * {@link #entries} queue if there is room. Acts as producer.<br/>
- * Later {@link PageSavingRunnable} reads in blocking manner from {@link #entries} and saves each
- * entry. Acts as consumer.
- * </p>
- * It starts only one instance of {@link PageSavingRunnable} because all we need is to make the page
- * storing asynchronous. We don't want to write concurrently in the wrapped {@link IDataStore},
- * though it may happen in the extreme case when the queue is full. These cases should be avoided.
- *
- * @author Matej Knopp
- */
-public class AsynchronousDataStore implements IDataStore
-{
-	/** Log for reporting. */
-	private static final Logger log = LoggerFactory.getLogger(AsynchronousDataStore.class);
-
-	/**
-	 * The time to wait when adding an {@link Entry} into the entries. In millis.
-	 */
-	private static final long OFFER_WAIT = 30L;
-
-	/**
-	 * The time to wait for an entry to save with the wrapped {@link IDataStore}. In millis.
-	 */
-	private static final long POLL_WAIT = 1000L;
-
-	/**
-	 * The page saving thread.
-	 */
-	private Thread pageSavingThread;
-
-	/**
-	 * The wrapped {@link IDataStore} that actually stores that pages
-	 */
-	private final IDataStore dataStore;
-
-	/**
-	 * The queue where the entries which have to be saved are temporary stored
-	 */
-	private final BlockingQueue<Entry> entries;
-
-	/**
-	 * A map 'sessionId:::pageId' -> {@link Entry}. Used for fast retrieval of {@link Entry}s which
-	 * are not yet stored by the wrapped {@link IDataStore}
-	 */
-	private final ConcurrentMap<String, Entry> entryMap;
-
-	/**
-	 * Construct.
-	 *
-	 * @param dataStore
-	 *            the wrapped {@link IDataStore} that actually saved the data
-	 * @param capacity
-	 *            the capacity of the queue that delays the saving
-	 */
-	public AsynchronousDataStore(final IDataStore dataStore, final int capacity)
-	{
-		this.dataStore = dataStore;
-		entries = new LinkedBlockingQueue<>(capacity);
-		entryMap = new ConcurrentHashMap<>();
-
-		pageSavingThread = new Thread(new PageSavingRunnable(), "Wicket-AsyncDataStore-PageSavingThread");
-		pageSavingThread.setDaemon(true);
-		pageSavingThread.start();
-	}
-
-	@Override
-	public void destroy()
-	{
-		final Thread thread = pageSavingThread;
-		pageSavingThread = null;
-		if (thread != null && thread.isAlive())
-		{
-			try
-			{
-				thread.join();
-			} catch (InterruptedException e)
-			{
-				log.error(e.getMessage(), e);
-			}
-		}
-
-		dataStore.destroy();
-	}
-
-	/**
-	 * Little helper
-	 *
-	 * @param sessionId
-	 * @param id
-	 * @return Entry
-	 */
-	private Entry getEntry(final String sessionId, final int id)
-	{
-		return entryMap.get(getKey(sessionId, id));
-	}
-
-	@Override
-	public byte[] getData(final String sessionId, final int id)
-	{
-		Entry entry = getEntry(sessionId, id);
-		if (entry != null)
-		{
-			log.debug(
-				"Returning the data of a non-stored entry with sessionId '{}' and pageId '{}'",
-				sessionId, id);
-			return entry.data;
-		}
-		byte[] data = dataStore.getData(sessionId, id);
-
-		log.debug("Returning the data of a stored entry with sessionId '{}' and pageId '{}'",
-			sessionId, id);
-
-		return data;
-	}
-
-	@Override
-	public boolean isReplicated()
-	{
-		return dataStore.isReplicated();
-	}
-
-	@Override
-	public void removeData(final String sessionId, final int id)
-	{
-		String key = getKey(sessionId, id);
-		if (key != null)
-		{
-			Entry entry = entryMap.remove(key);
-			if (entry != null)
-			{
-				entries.remove(entry);
-			}
-		}
-
-		dataStore.removeData(sessionId, id);
-	}
-
-	@Override
-	public void removeData(final String sessionId)
-	{
-		for (Iterator<Entry> itor = entries.iterator(); itor.hasNext();)
-		{
-			Entry entry = itor.next();
-			if (entry != null) // this check is not needed in JDK6
-			{
-				String entrySessionId = entry.sessionId;
-
-				if (sessionId.equals(entrySessionId))
-				{
-					entryMap.remove(getKey(entry));
-					itor.remove();
-				}
-			}
-		}
-
-		dataStore.removeData(sessionId);
-	}
-
-	/**
-	 * Save the entry in the queue if there is a room or directly pass it to the wrapped
-	 * {@link IDataStore} if there is no such
-	 *
-	 * @see org.apache.wicket.pageStore.IDataStore#storeData(java.lang.String, int, byte[])
-	 */
-	@Override
-	public void storeData(final String sessionId, final int id, final byte[] data)
-	{
-		if (pageSavingThread == null)
-		{
-			return;
-		}
-		Entry entry = new Entry(sessionId, id, data);
-		String key = getKey(entry);
-		entryMap.put(key, entry);
-
-		try
-		{
-			boolean added = entries.offer(entry, OFFER_WAIT, TimeUnit.MILLISECONDS);
-
-			if (added == false)
-			{
-				log.debug("Storing synchronously data with id '{}' in session '{}'", id, sessionId);
-				entryMap.remove(key);
-				dataStore.storeData(sessionId, id, data);
-			}
-		}
-		catch (InterruptedException e)
-		{
-			log.error(e.getMessage(), e);
-			if (pageSavingThread != null)
-			{
-				entryMap.remove(key);
-				dataStore.storeData(sessionId, id, data);
-			}
-		}
-	}
-
-	/**
-	 *
-	 * @param pageId
-	 * @param sessionId
-	 * @return generated key
-	 */
-	private static String getKey(final String sessionId, final int pageId)
-	{
-		return pageId + ":::" + sessionId;
-	}
-
-	/**
-	 *
-	 * @param entry
-	 * @return generated key
-	 */
-	private static String getKey(final Entry entry)
-	{
-		return getKey(entry.sessionId, entry.pageId);
-	}
-
-	/**
-	 * The structure used for an entry in the queue
-	 */
-	private static class Entry
-	{
-		private final String sessionId;
-		private final int pageId;
-		private final byte data[];
-
-		public Entry(final String sessionId, final int pageId, final byte data[])
-		{
-			this.sessionId = Args.notNull(sessionId, "sessionId");
-			this.pageId = pageId;
-			this.data = Args.notNull(data, "data");
-		}
-
-		@Override
-		public int hashCode()
-		{
-			final int prime = 31;
-			int result = 1;
-			result = prime * result + pageId;
-			result = prime * result + ((sessionId == null) ? 0 : sessionId.hashCode());
-			return result;
-		}
-
-		@Override
-		public boolean equals(Object obj)
-		{
-			if (this == obj)
-				return true;
-			if (obj == null)
-				return false;
-			if (getClass() != obj.getClass())
-				return false;
-			Entry other = (Entry)obj;
-			if (pageId != other.pageId)
-				return false;
-			if (sessionId == null)
-			{
-				if (other.sessionId != null)
-					return false;
-			}
-			else if (!sessionId.equals(other.sessionId))
-				return false;
-			return true;
-		}
-
-		@Override
-		public String toString()
-		{
-			return "Entry [sessionId=" + sessionId + ", pageId=" + pageId + "]";
-		}
-
-	}
-
-	/**
-	 * The thread that acts as consumer of {@link Entry}ies
-	 */
-	private class PageSavingRunnable implements Runnable
-	{
-		@Override
-		public void run()
-		{
-			while (pageSavingThread != null)
-			{
-				Entry entry = null;
-				try
-				{
-					entry = entries.poll(POLL_WAIT, TimeUnit.MILLISECONDS);
-				}
-				catch (InterruptedException e)
-				{
-					log.debug("PageSavingRunnable:: Interrupted...");
-				}
-
-				if (entry != null && pageSavingThread != null)
-				{
-					log.debug("PageSavingRunnable:: Saving asynchronously: {}...", entry);
-					dataStore.storeData(entry.sessionId, entry.pageId, entry.data);
-					entryMap.remove(getKey(entry));
-				}
-			}
-		}
-	}
-
-	@Override
-	public final boolean canBeAsynchronous()
-	{
-		// should not wrap in another AsynchronousDataStore
-		return false;
-	}
-}

http://git-wip-us.apache.org/repos/asf/wicket/blob/c43d3a33/wicket-core/src/main/java/org/apache/wicket/pageStore/AsynchronousPageStore.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/pageStore/AsynchronousPageStore.java b/wicket-core/src/main/java/org/apache/wicket/pageStore/AsynchronousPageStore.java
index c24b2a0..0d23925 100644
--- a/wicket-core/src/main/java/org/apache/wicket/pageStore/AsynchronousPageStore.java
+++ b/wicket-core/src/main/java/org/apache/wicket/pageStore/AsynchronousPageStore.java
@@ -17,41 +17,43 @@
 package org.apache.wicket.pageStore;
 
 import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
 
+import org.apache.wicket.MetaDataKey;
+import org.apache.wicket.WicketRuntimeException;
 import org.apache.wicket.page.IManageablePage;
 import org.apache.wicket.util.lang.Args;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * Facade for {@link IPageStore} that does the actual saving in worker thread.
+ * Facade for {@link IPageStore} moving {@link #addPage(IPageContext, IManageablePage)} to a worker thread.
  * <p>
- * Creates an {@link Entry} for each double (sessionId, page) and puts it in {@link #entries} queue
- * if there is room. Acts as producer.<br/>
- * Later {@link PageSavingRunnable} reads in blocking manner from {@link #entries} and saves each
- * entry. Acts as consumer.
- * </p>
- * It starts only one instance of {@link PageSavingRunnable} because all we need is to make the page
+ * Creates an {@link PendingAdd} for {@link #addPage(IPageContext, IManageablePage)} and puts ito a {@link #queue}.
+ * Later {@link PageAddingRunnable} reads in blocking manner from {@link #queue} and performs the add.
+ * <p>
+ * It starts only one instance of {@link PageAddingRunnable} because all we need is to make the page
  * storing asynchronous. We don't want to write concurrently in the wrapped {@link IPageStore},
  * though it may happen in the extreme case when the queue is full. These cases should be avoided.
- *
- * Based on AsynchronousDataStore (@author Matej Knopp).
- *
+ * 
+ * @author Matej Knopp
  * @author manuelbarzi
  */
-public class AsynchronousPageStore implements IPageStore
+public class AsynchronousPageStore extends DelegatingPageStore
 {
 
 	/** Log for reporting. */
 	private static final Logger log = LoggerFactory.getLogger(AsynchronousPageStore.class);
 
 	/**
-	 * The time to wait when adding an {@link Entry} into the entries. In millis.
+	 * The time to wait when adding an {@link PendingAdd} into the entries. In millis.
 	 */
 	private static final long OFFER_WAIT = 30L;
 
@@ -63,27 +65,22 @@ public class AsynchronousPageStore implements IPageStore
 	/**
 	 * The page saving thread.
 	 */
-	private Thread pageSavingThread;
-
-	/**
-	 * The wrapped {@link IPageStore} that actually stores that pages
-	 */
-	private final IPageStore delegate;
+	private final Thread pageSavingThread;
 
 	/**
 	 * The queue where the entries which have to be saved are temporary stored
 	 */
-	private final BlockingQueue<Entry> entries;
+	private final BlockingQueue<PendingAdd> queue;
 
 	/**
-	 * A map 'sessionId:::pageId' -> {@link Entry}. Used for fast retrieval of {@link Entry}s which
+	 * A map 'sessionId:::pageId' -> {@link PendingAdd}. Used for fast retrieval of {@link PendingAdd}s which
 	 * are not yet stored by the wrapped {@link IPageStore}
 	 */
-	private final ConcurrentMap<String, Entry> entryMap;
+	private final ConcurrentMap<String, PendingAdd> queueMap;
 
 	/**
 	 * Construct.
-	 *
+	 * 
 	 * @param delegate
 	 *            the wrapped {@link IPageStore} that actually saved the page
 	 * @param capacity
@@ -91,29 +88,19 @@ public class AsynchronousPageStore implements IPageStore
 	 */
 	public AsynchronousPageStore(final IPageStore delegate, final int capacity)
 	{
-		this.delegate = Args.notNull(delegate, "delegate");
-		entries = new LinkedBlockingQueue<>(capacity);
-		entryMap = new ConcurrentHashMap<>();
+		super(delegate);
+		
+		queue = new LinkedBlockingQueue<>(capacity);
+		queueMap = new ConcurrentHashMap<>();
 
-		pageSavingThread = new Thread(new PageSavingRunnable(), "Wicket-AsyncPageStore-PageSavingThread");
+		PageAddingRunnable savingRunnable = new PageAddingRunnable(delegate, queue, queueMap);
+		pageSavingThread = new Thread(savingRunnable, "Wicket-AsyncPageStore-PageSavingThread");
 		pageSavingThread.setDaemon(true);
 		pageSavingThread.start();
 	}
 
 	/**
-	 * Little helper
-	 *
-	 * @param sessionId
-	 * @param pageId
-	 * @return Entry
-	 */
-	private Entry getEntry(final String sessionId, final int pageId)
-	{
-		return entryMap.get(getKey(sessionId, pageId));
-	}
-
-	/**
-	 *
+	 * 
 	 * @param pageId
 	 * @param sessionId
 	 * @return generated key
@@ -124,89 +111,210 @@ public class AsynchronousPageStore implements IPageStore
 	}
 
 	/**
-	 *
-	 * @param entry
-	 * @return generated key
+	 * An add of a page that is pending its asynchronous execution.
+	 * <p>
+	 * Used as an isolating {@link IPageContext} for the delegation to 
+	 * {@link IPageStore#addPage(IPageContext, IManageablePage)}.
 	 */
-	private static String getKey(final Entry entry)
+	private static class PendingAdd implements IPageContext
 	{
-		return getKey(entry.sessionId, entry.page.getPageId());
-	}
+		private final IPageContext context;
+		
+		private final IManageablePage page;
 
-	/**
-	 * The structure used for an entry in the queue
-	 */
-	private static class Entry
-	{
 		private final String sessionId;
-		private final IManageablePage page;
 
-		public Entry(final String sessionId, final IManageablePage page)
+		/**
+		 * Is this context passed to an asynchronously called {@link IPageStore#addPage(IPageContext, IManageablePage)}.
+		 */
+		private boolean asynchronous = false;
+
+		/**
+		 * Cache of session attributes which may filled in {@link IPageStore#canBeAsynchronous(IPageContext)},
+		 * so these are available asynchronously later on.
+		 */
+		private final Map<String, Serializable> attributeCache = new HashMap<>();
+
+		public PendingAdd(final IPageContext context, final IManageablePage page)
 		{
-			this.sessionId = Args.notNull(sessionId, "sessionId");
+			this.context = Args.notNull(context, "context");
 			this.page = Args.notNull(page, "page");
+			
+			context.bind();
+			this.sessionId = context.getSessionId();
+		}
+
+		/**
+		 * 
+		 * @param entry
+		 * @return generated key
+		 */
+		private String getKey()
+		{
+			return AsynchronousPageStore.getKey(sessionId, page.getPageId());
 		}
 
 		@Override
-		public int hashCode()
+		public String toString()
 		{
-			final int prime = 31;
-			int result = 1;
-			result = prime * result + page.getPageId();
-			result = prime * result + sessionId.hashCode();
-			return result;
+			return "PendingAdd [sessionId=" + sessionId + ", pageId=" + page.getPageId() + "]";
 		}
 
+		/**
+		 * Prevents access to request when called asynchronously.
+		 */
 		@Override
-		public boolean equals(Object obj)
+		public <T> void setRequestData(MetaDataKey<T> key, T value)
 		{
-			if (this == obj)
-				return true;
-			if (obj == null)
-				return false;
-			if (getClass() != obj.getClass())
-				return false;
-			Entry other = (Entry)obj;
-			if (page.getPageId() != other.page.getPageId())
-				return false;
-			if (!sessionId.equals(other.sessionId))
-				return false;
-			return true;
+			if (asynchronous) {
+				throw new WicketRuntimeException("no request available asynchronuously");
+			}
+			
+			context.setRequestData(key, value);
 		}
 
+		/**
+		 * Prevents access to request when called asynchronously.
+		 */
 		@Override
-		public String toString()
+		public <T> T getRequestData(MetaDataKey<T> key)
+		{
+			if (asynchronous) {
+				throw new WicketRuntimeException("no request available asynchronuously");
+			}
+			
+			return context.getRequestData(key);
+		}
+
+		/**
+		 * Prevents access to the session when called asynchronously.
+		 * <p>
+		 * All values set from {@link IPageStore#canBeAsynchronous(IPageContext)} are kept
+		 * for later retrieval.
+		 */
+		@Override
+		public <T extends Serializable> void setSessionAttribute(String key, T value)
+		{
+			if (asynchronous) {
+				throw new WicketRuntimeException("no session available asynchronuously");
+			}
+			
+			if (value != null) {
+				attributeCache.put(key, value);
+			}
+
+			context.setSessionAttribute(key, value);
+		}
+		
+		/**
+		 * Prevents access to the session when called asynchronously.
+		 * <p>
+		 * All values set from {@link IPageStore#canBeAsynchronous(IPageContext)} are still
+		 * available.
+		 */
+		@SuppressWarnings("unchecked")
+		@Override
+		public <T extends Serializable> T getSessionAttribute(String key)
+		{
+			if (asynchronous) {
+				T value = (T)attributeCache.get(key);
+				if (value != null) {
+					return value;
+				}
+				
+				throw new WicketRuntimeException("no session available asynchronuously");
+			}
+			
+			T value = context.getSessionAttribute(key);
+			if (value != null) {
+				attributeCache.put(key, value);
+			}
+			
+			return value;
+		}
+		
+		/**
+		 * Prevents access to the session when called asynchronously.
+		 */
+		@Override
+		public <T extends Serializable> T setSessionData(MetaDataKey<T> key, T value)
+		{
+			if (asynchronous) {
+				throw new WicketRuntimeException("no session available asynchronuously");
+			}
+			
+			return context.setSessionData(key, value);
+		}
+
+		/**
+		 * Gets data from the session.
+		 */
+		@Override
+		public <T extends Serializable> T getSessionData(MetaDataKey<T> key)
 		{
-			return "Entry [sessionId=" + sessionId + ", pageId=" + page.getPageId() + "]";
+			return context.getSessionData(key);
 		}
 
+		/**
+		 * Has no effect if already bound.
+		 */
+		@Override
+		public void bind()
+		{
+		}
+
+		/**
+		 * Returns id of session.
+		 */
+		@Override
+		public String getSessionId()
+		{
+			return sessionId;
+		}
 	}
 
 	/**
-	 * The thread that acts as consumer of {@link Entry}ies
+	 * The consumer of {@link PendingAdd}s.
 	 */
-	private class PageSavingRunnable implements Runnable
+	private static class PageAddingRunnable implements Runnable
 	{
+		private static final Logger log = LoggerFactory.getLogger(PageAddingRunnable.class);
+
+		private final BlockingQueue<PendingAdd> entries;
+
+		private final ConcurrentMap<String, PendingAdd> addQueue;
+
+		private final IPageStore delegate;
+
+		private PageAddingRunnable(IPageStore delegate, BlockingQueue<PendingAdd> entries,
+		                           ConcurrentMap<String, PendingAdd> entryMap)
+		{
+			this.delegate = delegate;
+			this.entries = entries;
+			this.addQueue = entryMap;
+		}
+
 		@Override
 		public void run()
 		{
-			while (pageSavingThread != null)
+			while (!Thread.interrupted())
 			{
-				Entry entry = null;
+				PendingAdd add = null;
 				try
 				{
-					entry = entries.poll(POLL_WAIT, TimeUnit.MILLISECONDS);
+					add = entries.poll(POLL_WAIT, TimeUnit.MILLISECONDS);
 				}
 				catch (InterruptedException e)
 				{
-					log.debug("PageSavingRunnable:: Interrupted...");
+					Thread.currentThread().interrupt();
 				}
 
-				if (entry != null && pageSavingThread != null)
+				if (add != null)
 				{
-					log.debug("PageSavingRunnable:: Saving asynchronously: {}...", entry);
-					delegate.storePage(entry.sessionId, entry.page);
-					entryMap.remove(getKey(entry));
+					log.debug("Saving asynchronously: {}...", add);
+					add.asynchronous = true;					
+					delegate.addPage(add, add.page);
+					addQueue.remove(add.getKey());
 				}
 			}
 		}
@@ -215,122 +323,98 @@ public class AsynchronousPageStore implements IPageStore
 	@Override
 	public void destroy()
 	{
-		final Thread thread = pageSavingThread;
-		pageSavingThread = null;
-		if (thread != null && thread.isAlive())
+		if (pageSavingThread.isAlive())
 		{
+			pageSavingThread.interrupt();
 			try
 			{
-				thread.join();
+				pageSavingThread.join();
 			}
 			catch (InterruptedException e)
 			{
 				log.error(e.getMessage(), e);
 			}
 		}
-		delegate.destroy();
+
+		super.destroy();
 	}
 
 	@Override
-	public IManageablePage getPage(String sessionId, int pageId)
+	public IManageablePage getPage(IPageContext context, int pageId)
 	{
-		Entry entry = getEntry(sessionId, pageId);
+		PendingAdd entry = queueMap.get(getKey(context.getSessionId(), pageId));
 		if (entry != null)
 		{
-			log.debug(
-				"Returning the page of a non-stored entry with session id '{}' and page id '{}'",
-				sessionId, pageId);
+			log.debug("Returning the page of a non-stored entry with page id '{}'", pageId);
 			return entry.page;
 		}
-		IManageablePage page = delegate.getPage(sessionId, pageId);
+		IManageablePage page = super.getPage(context, pageId);
 
-		log.debug("Returning the page of a stored entry with session id '{}' and page id '{}'",
-			sessionId, pageId);
+		log.debug("Returning the page of a stored entry with page id '{}'", pageId);
 
 		return page;
 	}
 
 	@Override
-	public void removePage(String sessionId, int pageId)
+	public void removePage(IPageContext context, IManageablePage page)
 	{
-		String key = getKey(sessionId, pageId);
+		String key = getKey(context.getSessionId(), page.getPageId());
 		if (key != null)
 		{
-			Entry entry = entryMap.remove(key);
+			PendingAdd entry = queueMap.remove(key);
 			if (entry != null)
 			{
-				entries.remove(entry);
+				queue.remove(entry);
 			}
 		}
 
-		delegate.removePage(sessionId, pageId);
+		super.removePage(context, page);
 	}
 
 	@Override
-	public void storePage(String sessionId, IManageablePage page)
+	public void addPage(IPageContext context, IManageablePage page)
 	{
-		if (pageSavingThread == null)
-		{
-			return;
-		}
-		Entry entry = new Entry(sessionId, page);
-		String key = getKey(entry);
-		entryMap.put(key, entry);
-
-		try
-		{
-			if (entries.offer(entry, OFFER_WAIT, TimeUnit.MILLISECONDS))
-			{
-				log.debug("Offered for storing asynchronously page with id '{}' in session '{}'",
-					page.getPageId(), sessionId);
-			}
-			else
+		PendingAdd add = new PendingAdd(context, page);
+		if (getDelegate().canBeAsynchronous(add)) {
+			String key = add.getKey();
+			queueMap.put(key, add);
+			try
 			{
-				log.debug("Storing synchronously page with id '{}' in session '{}'",
-					page.getPageId(), sessionId);
-				entryMap.remove(key);
-				delegate.storePage(sessionId, page);
+				if (queue.offer(add, OFFER_WAIT, TimeUnit.MILLISECONDS))
+				{
+					log.debug("Offered for storing asynchronously page with id '{}'", page.getPageId());
+					return;
+				}
+				else
+				{
+					log.debug("Storing synchronously page with id '{}'", page.getPageId());
+					queueMap.remove(key);
+				}
 			}
-		}
-		catch (InterruptedException e)
-		{
-			log.error(e.getMessage(), e);
-			if (pageSavingThread != null)
+			catch (InterruptedException e)
 			{
-				entryMap.remove(key);
-				delegate.storePage(sessionId, page);
+				log.error(e.getMessage(), e);
+				queueMap.remove(key);
 			}
+		} else {
+			log.warn("Delegated page store '{}' can not be asynchronous", getDelegate().getClass().getName());
 		}
+		
+		super.addPage(context, page);
 	}
 
 	@Override
-	public void unbind(String sessionId)
-	{
-		delegate.unbind(sessionId);
-	}
-
-	@Override
-	public Serializable prepareForSerialization(String sessionId, Serializable page)
-	{
-		return delegate.prepareForSerialization(sessionId, page);
-	}
-
-	@Override
-	public Object restoreAfterSerialization(Serializable serializable)
-	{
-		return delegate.restoreAfterSerialization(serializable);
-	}
-
-	@Override
-	public IManageablePage convertToPage(Object page)
+	public void removeAllPages(IPageContext context)
 	{
-		return delegate.convertToPage(page);
-	}
-
-	@Override
-	public boolean canBeAsynchronous()
-	{
-		// should not wrap in another AsynchronousPageStore
-		return false;
+		Iterator<PendingAdd> iterator = queue.iterator();
+		while (iterator.hasNext()) {
+			PendingAdd add = iterator.next(); 
+		
+			if (add.sessionId.equals(context.getSessionId())) {
+				iterator.remove();
+			}
+		}
+		
+		super.removeAllPages(context);
 	}
-}
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/wicket/blob/c43d3a33/wicket-core/src/main/java/org/apache/wicket/pageStore/DefaultPageContext.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/pageStore/DefaultPageContext.java b/wicket-core/src/main/java/org/apache/wicket/pageStore/DefaultPageContext.java
new file mode 100644
index 0000000..971866e
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/pageStore/DefaultPageContext.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.pageStore;
+
+import java.io.Serializable;
+
+import org.apache.wicket.MetaDataKey;
+import org.apache.wicket.Session;
+import org.apache.wicket.request.cycle.RequestCycle;
+
+/**
+ * Default page context using a {@link Session}.
+ * 
+ * @author Juergen Donnerstag
+ * @author svenmeier
+ */
+public class DefaultPageContext implements IPageContext
+{
+	private Session session;
+
+	public DefaultPageContext(Session session) {
+		this.session = Session.get();
+	}
+	
+	/**
+	 * @see org.apache.wicket.pageStore.IPageContext#bind()
+	 */
+	@Override
+	public void bind()
+	{
+		session.bind();
+	}
+
+	/**
+	 * @see org.apache.wicket.pageStore.IPageContext#getSessionId()
+	 */
+	@Override
+	public String getSessionId()
+	{
+		return session.getId();
+	}
+
+	@SuppressWarnings("unchecked")
+	@Override
+	public <T extends Serializable> T getSessionAttribute(String key)
+	{
+		return (T)session.getAttribute(key);
+	}
+	
+	@Override
+	public <T extends Serializable> void setSessionAttribute(String key, T value)
+	{
+		session.setAttribute(key, value);
+	}
+	
+	@Override
+	public <T extends Serializable> T getSessionData(MetaDataKey<T> key)
+	{
+		return session.getMetaData(key);
+	}
+
+	@Override
+	public <T extends Serializable> T setSessionData(MetaDataKey<T> key, T value)
+	{
+		synchronized (session)
+		{
+			T oldValue = session.getMetaData(key);
+			if (oldValue != null) {
+				return oldValue;
+			}
+			
+			session.setMetaData(key, value);
+			return value;
+		}
+	}
+
+	@Override
+	public <T> T getRequestData(MetaDataKey<T> key)
+	{
+		RequestCycle requestCycle = RequestCycle.get();
+		if (requestCycle == null)
+		{
+			throw new IllegalStateException("Not a request thread.");
+		}
+		return requestCycle.getMetaData(key);
+	}
+
+	@Override
+	public <T> void setRequestData(MetaDataKey<T> key, T value)
+	{
+		RequestCycle requestCycle = RequestCycle.get();
+		if (requestCycle == null)
+		{
+			throw new IllegalStateException("Not a request thread.");
+		}
+		requestCycle.setMetaData(key, value);
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/c43d3a33/wicket-core/src/main/java/org/apache/wicket/pageStore/DefaultPageStore.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/pageStore/DefaultPageStore.java b/wicket-core/src/main/java/org/apache/wicket/pageStore/DefaultPageStore.java
deleted file mode 100644
index f324b85..0000000
--- a/wicket-core/src/main/java/org/apache/wicket/pageStore/DefaultPageStore.java
+++ /dev/null
@@ -1,469 +0,0 @@
-/*
- * 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.pageStore;
-
-import java.io.Serializable;
-import java.lang.ref.SoftReference;
-import java.util.Iterator;
-import java.util.concurrent.ConcurrentLinkedDeque;
-
-import org.apache.wicket.page.IManageablePage;
-import org.apache.wicket.serialize.ISerializer;
-import org.apache.wicket.util.lang.Args;
-import org.apache.wicket.util.lang.Objects;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * The {@link IPageStore} that converts {@link IManageablePage} instances to {@link SerializedPage}s
- * before passing them to the {@link IDataStore} to store them and the same in the opposite
- * direction when loading {@link SerializedPage} from the data store.
- * 
- */
-public class DefaultPageStore extends AbstractCachingPageStore<DefaultPageStore.SerializedPage>
-{
-	private static final Logger LOG = LoggerFactory.getLogger(DefaultPageStore.class);
-
-	/**
-	 * Construct.
-	 * 
-	 * @param pageSerializer
-	 *            the {@link ISerializer} that will be used to convert pages from/to byte arrays
-	 * @param dataStore
-	 *            the {@link IDataStore} that actually stores the pages
-	 * @param cacheSize
-	 *            the number of pages to cache in memory before passing them to
-	 *            {@link IDataStore#storeData(String, int, byte[])}
-	 */
-	public DefaultPageStore(final ISerializer pageSerializer, final IDataStore dataStore,
-		final int cacheSize)
-	{
-		super(pageSerializer, dataStore, new SerializedPagesCache(cacheSize));
-	}
-
-	@Override
-	public void storePage(final String sessionId, final IManageablePage page)
-	{
-		SerializedPage serialized = createSerializedPage(sessionId, page);
-		if (serialized != null)
-		{
-			int pageId = page.getPageId();
-			pagesCache.storePage(sessionId, pageId, serialized);
-			storePageData(sessionId, pageId, serialized.getData());
-		}
-	}
-
-	@Override
-	public IManageablePage convertToPage(final Object object)
-	{
-		if (object == null)
-		{
-			return null;
-		}
-		else if (object instanceof IManageablePage)
-		{
-			return (IManageablePage)object;
-		}
-		else if (object instanceof SerializedPage)
-		{
-			SerializedPage page = (SerializedPage)object;
-			byte data[] = page.getData();
-			if (data == null)
-			{
-				data = getPageData(page.getSessionId(), page.getPageId());
-			}
-			if (data != null)
-			{
-				return deserializePage(data);
-			}
-			return null;
-		}
-
-		String type = object.getClass().getName();
-		throw new IllegalArgumentException("Unknown object type " + type);
-	}
-
-	/**
-	 * Reloads the {@link SerializedPage} from the backing {@link IDataStore} if the
-	 * {@link SerializedPage#data} is stripped earlier
-	 * 
-	 * @param serializedPage
-	 *            the {@link SerializedPage} with empty {@link SerializedPage#data} slot
-	 * @return the fully functional {@link SerializedPage}
-	 */
-	private SerializedPage restoreStrippedSerializedPage(final SerializedPage serializedPage)
-	{
-		SerializedPage result = pagesCache.getPage(serializedPage.getSessionId(),
-			serializedPage.getPageId());
-		if (result != null)
-		{
-			return result;
-		}
-
-		byte data[] = getPageData(serializedPage.getSessionId(), serializedPage.getPageId());
-		return new SerializedPage(serializedPage.getSessionId(), serializedPage.getPageId(), data);
-	}
-
-	@Override
-	public Serializable prepareForSerialization(final String sessionId, final Serializable page)
-	{
-		if (dataStore.isReplicated())
-		{
-			return null;
-		}
-
-		SerializedPage result = null;
-
-		if (page instanceof IManageablePage)
-		{
-			IManageablePage _page = (IManageablePage)page;
-			result = pagesCache.getPage(sessionId, _page.getPageId());
-			if (result == null)
-			{
-				result = createSerializedPage(sessionId, _page);
-				if (result != null)
-				{
-					pagesCache.storePage(sessionId, _page.getPageId(), result);
-				}
-			}
-		}
-		else if (page instanceof SerializedPage)
-		{
-			SerializedPage _page = (SerializedPage)page;
-			if (_page.getData() == null)
-			{
-				result = restoreStrippedSerializedPage(_page);
-			}
-			else
-			{
-				result = _page;
-			}
-		}
-
-		if (result != null)
-		{
-			return result;
-		}
-		return page;
-	}
-
-	/**
-	 * 
-	 * @return Always true for this implementation
-	 */
-	protected boolean storeAfterSessionReplication()
-	{
-		return true;
-	}
-
-	@Override
-	public Object restoreAfterSerialization(final Serializable serializable)
-	{
-		if (serializable == null)
-		{
-			return null;
-		}
-		else if (!storeAfterSessionReplication() || serializable instanceof IManageablePage)
-		{
-			return serializable;
-		}
-		else if (serializable instanceof SerializedPage)
-		{
-			SerializedPage page = (SerializedPage)serializable;
-			if (page.getData() != null)
-			{
-				storePageData(page.getSessionId(), page.getPageId(), page.getData());
-				return new SerializedPage(page.getSessionId(), page.getPageId(), null);
-			}
-			return page;
-		}
-
-		String type = serializable.getClass().getName();
-		throw new IllegalArgumentException("Unknown object type " + type);
-	}
-
-	/**
-	 * A representation of {@link IManageablePage} that knows additionally the id of the http
-	 * session in which this {@link IManageablePage} instance is used. The {@link #sessionId} and
-	 * {@link #pageId} are used for better clustering in the {@link IDataStore} structures.
-	 */
-	protected static class SerializedPage implements Serializable
-	{
-		private static final long serialVersionUID = 1L;
-
-		/**
-		 * The id of the serialized {@link IManageablePage}
-		 */
-		private final int pageId;
-
-		/**
-		 * The id of the http session in which the serialized {@link IManageablePage} is used.
-		 */
-		private final String sessionId;
-
-		/**
-		 * The serialized {@link IManageablePage}
-		 */
-		private final byte[] data;
-
-		public SerializedPage(String sessionId, int pageId, byte[] data)
-		{
-			this.pageId = pageId;
-			this.sessionId = sessionId;
-			this.data = data;
-		}
-
-		public byte[] getData()
-		{
-			return data;
-		}
-
-		public int getPageId()
-		{
-			return pageId;
-		}
-
-		public String getSessionId()
-		{
-			return sessionId;
-		}
-
-		@Override
-		public boolean equals(Object obj)
-		{
-			if (this == obj)
-			{
-				return true;
-			}
-			if ((obj instanceof SerializedPage) == false)
-			{
-				return false;
-			}
-			SerializedPage rhs = (SerializedPage)obj;
-			return Objects.equal(getPageId(), rhs.getPageId()) &&
-				Objects.equal(getSessionId(), rhs.getSessionId());
-		}
-
-		@Override
-		public int hashCode()
-		{
-			return Objects.hashCode(getPageId(), getSessionId());
-		}
-	}
-
-	/**
-	 * 
-	 * @param sessionId
-	 * @param page
-	 * @return the serialized page information
-	 */
-	protected SerializedPage createSerializedPage(final String sessionId, final IManageablePage page)
-	{
-		Args.notNull(sessionId, "sessionId");
-		Args.notNull(page, "page");
-
-		SerializedPage serializedPage = null;
-
-		byte[] data = serializePage(page);
-
-		if (data != null)
-		{
-			serializedPage = new SerializedPage(sessionId, page.getPageId(), data);
-		}
-		else if (LOG.isWarnEnabled())
-		{
-			LOG.warn("Page {} cannot be serialized. See previous logs for possible reasons.", page);
-		}
-		return serializedPage;
-	}
-
-	/**
-	 * Cache that stores serialized pages. This is important to make sure that a single page is not
-	 * serialized twice or more when not necessary.
-	 * <p>
-	 * For example a page is serialized during request, but it might be also later serialized on
-	 * session replication. The purpose of this cache is to make sure that the data obtained from
-	 * first serialization is reused on second serialization.
-	 * 
-	 * @author Matej Knopp
-	 */
-	static class SerializedPagesCache implements SecondLevelPageCache<String, Integer, SerializedPage>
-	{
-		private final int maxSize;
-
-		private final ConcurrentLinkedDeque<SoftReference<SerializedPage>> cache;
-
-		/**
-		 * Constructor.
-		 * 
-		 * @param maxSize
-		 *          The maximum number of entries to cache
-		 */
-		public SerializedPagesCache(final int maxSize)
-		{
-			this.maxSize = maxSize;
-			cache = new ConcurrentLinkedDeque<>();
-		}
-
-		/**
-		 * 
-		 * @param sessionId
-		 * @param pageId
-		 * @return the removed {@link SerializedPage} or <code>null</code> - otherwise
-		 */
-		@Override
-		public SerializedPage removePage(final String sessionId, final Integer pageId)
-		{
-			if (maxSize > 0)
-			{
-				Args.notNull(sessionId, "sessionId");
-				Args.notNull(pageId, "pageId");
-
-				SerializedPage sample = new SerializedPage(sessionId, pageId, null);
-
-				for (Iterator<SoftReference<SerializedPage>> i = cache.iterator(); i.hasNext();)
-				{
-					SoftReference<SerializedPage> ref = i.next();
-					SerializedPage entry = ref.get();
-					if (sample.equals(entry))
-					{
-						i.remove();
-						return entry;
-					}
-				}
-			}
-			return null;
-		}
-
-		/**
-		 * Removes all {@link SerializedPage}s for the session with <code>sessionId</code> from the
-		 * cache.
-		 * 
-		 * @param sessionId
-		 */
-		@Override
-		public void removePages(String sessionId)
-		{
-			if (maxSize > 0)
-			{
-				Args.notNull(sessionId, "sessionId");
-
-				for (Iterator<SoftReference<SerializedPage>> i = cache.iterator(); i.hasNext();)
-				{
-					SoftReference<SerializedPage> ref = i.next();
-					SerializedPage entry = ref.get();
-					if (entry != null && entry.getSessionId().equals(sessionId))
-					{
-						i.remove();
-					}
-				}
-			}
-		}
-
-		/**
-		 * Returns a {@link SerializedPage} by looking it up by <code>sessionId</code> and
-		 * <code>pageId</code>. If there is a match then it is <i>touched</i>, i.e. it is moved at
-		 * the top of the cache.
-		 * 
-		 * @param sessionId
-		 * @param pageId
-		 * @return the found serialized page or <code>null</code> when not found
-		 */
-		@Override
-		public SerializedPage getPage(String sessionId, Integer pageId)
-		{
-			SerializedPage result = null;
-			if (maxSize > 0)
-			{
-				Args.notNull(sessionId, "sessionId");
-				Args.notNull(pageId, "pageId");
-
-				SerializedPage sample = new SerializedPage(sessionId, pageId, null);
-
-				for (Iterator<SoftReference<SerializedPage>> i = cache.iterator(); i.hasNext();)
-				{
-					SoftReference<SerializedPage> ref = i.next();
-					SerializedPage entry = ref.get();
-					if (sample.equals(entry))
-					{
-						i.remove();
-						result = entry;
-						break;
-					}
-				}
-
-				if (result != null)
-				{
-					// move to top
-					internalStore(result);
-				}
-			}
-			return result;
-		}
-
-		/**
-		 * Store the serialized page in cache
-		 * 
-		 * @param page
-		 *      the data to serialize (page id, session id, bytes)
-		 */
-		@Override
-		public void storePage(String sessionId, Integer pageId, SerializedPage page)
-		{
-			if (maxSize > 0)
-			{
-				Args.notNull(sessionId, "sessionId");
-				Args.notNull(pageId, "pageId");
-				Args.notNull(page, "page");
-
-				for (Iterator<SoftReference<SerializedPage>> i = cache.iterator(); i.hasNext();)
-				{
-					SoftReference<SerializedPage> r = i.next();
-					SerializedPage entry = r.get();
-					if (entry != null && entry.equals(page))
-					{
-						i.remove();
-						break;
-					}
-				}
-
-				internalStore(page);
-			}
-		}
-
-		private void internalStore(SerializedPage page)
-		{
-			cache.push(new SoftReference<>(page));
-			while (cache.size() > maxSize)
-			{
-				cache.pollLast();
-			}
-		}
-
-		@Override
-		public void destroy()
-		{
-			cache.clear();
-		}
-	}
-
-	@Override
-	public boolean canBeAsynchronous()
-	{
-		return true;
-	}
-}

http://git-wip-us.apache.org/repos/asf/wicket/blob/c43d3a33/wicket-core/src/main/java/org/apache/wicket/pageStore/DelegatingPageStore.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/pageStore/DelegatingPageStore.java b/wicket-core/src/main/java/org/apache/wicket/pageStore/DelegatingPageStore.java
new file mode 100644
index 0000000..126509c
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/pageStore/DelegatingPageStore.java
@@ -0,0 +1,67 @@
+/*
+ * 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.pageStore;
+
+import org.apache.wicket.page.IManageablePage;
+import org.apache.wicket.util.lang.Args;
+
+/**
+ * An {@link IPageStore} that delegates to another storage.
+ */
+public abstract class DelegatingPageStore implements IPageStore
+{
+	private final IPageStore delegate;
+	
+	protected DelegatingPageStore(IPageStore delegate) {
+		this.delegate = Args.notNull(delegate, "delegate");
+	}
+
+	public IPageStore getDelegate()
+	{
+		return delegate;
+	}
+	
+	@Override
+	public void addPage(IPageContext context, IManageablePage page) {
+		delegate.addPage(context, page);
+	}
+
+	@Override
+	public void removePage(IPageContext context, IManageablePage page) {
+		delegate.removePage(context, page);
+	}
+
+	@Override
+	public void removeAllPages(IPageContext context) {
+		delegate.removeAllPages(context);
+	}
+	
+	@Override
+	public IManageablePage getPage(IPageContext context, int id) {
+		return delegate.getPage(context, id);
+	}
+
+	@Override
+	public void detach(IPageContext context) {
+		delegate.detach(context);
+	}
+
+	@Override
+	public void destroy() {
+		delegate.destroy();
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/c43d3a33/wicket-core/src/main/java/org/apache/wicket/pageStore/DiskDataStore.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/pageStore/DiskDataStore.java b/wicket-core/src/main/java/org/apache/wicket/pageStore/DiskDataStore.java
deleted file mode 100644
index 2dd95e4..0000000
--- a/wicket-core/src/main/java/org/apache/wicket/pageStore/DiskDataStore.java
+++ /dev/null
@@ -1,589 +0,0 @@
-/*
- * 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.pageStore;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.io.OutputStream;
-import java.io.RandomAccessFile;
-import java.io.Serializable;
-import java.nio.ByteBuffer;
-import java.nio.channels.FileChannel;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-
-import org.apache.wicket.WicketRuntimeException;
-import org.apache.wicket.pageStore.PageWindowManager.PageWindow;
-import org.apache.wicket.util.file.Files;
-import org.apache.wicket.util.io.IOUtils;
-import org.apache.wicket.util.lang.Args;
-import org.apache.wicket.util.lang.Bytes;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * A data store implementation which stores the data on disk (in a file system)
- */
-public class DiskDataStore implements IDataStore
-{
-	private static final Logger log = LoggerFactory.getLogger(DiskDataStore.class);
-
-	private static final String INDEX_FILE_NAME = "DiskDataStoreIndex";
-
-	private final String applicationName;
-
-	private final Bytes maxSizePerPageSession;
-
-	private final File fileStoreFolder;
-
-	private final ConcurrentMap<String, SessionEntry> sessionEntryMap;
-
-	/**
-	 * Construct.
-	 * 
-	 * @param applicationName
-	 * @param fileStoreFolder
-	 * @param maxSizePerSession
-	 */
-	public DiskDataStore(final String applicationName, final File fileStoreFolder,
-		final Bytes maxSizePerSession)
-	{
-		this.applicationName = applicationName;
-		this.fileStoreFolder = fileStoreFolder;
-		maxSizePerPageSession = Args.notNull(maxSizePerSession, "maxSizePerSession");
-		sessionEntryMap = new ConcurrentHashMap<>();
-
-		try
-		{
-			if (this.fileStoreFolder.exists() || this.fileStoreFolder.mkdirs())
-			{
-				loadIndex();
-			}
-			else
-			{
-				log.warn("Cannot create file store folder for some reason.");
-			}
-		}
-		catch (SecurityException e)
-		{
-			throw new WicketRuntimeException(
-				"SecurityException occurred while creating DiskDataStore. Consider using a non-disk based IDataStore implementation. "
-					+ "See org.apache.wicket.Application.setPageManagerProvider(IPageManagerProvider)",
-				e);
-		}
-	}
-
-	/**
-	 * @see org.apache.wicket.pageStore.IDataStore#destroy()
-	 */
-	@Override
-	public void destroy()
-	{
-		log.debug("Destroying...");
-		saveIndex();
-		log.debug("Destroyed.");
-	}
-
-	/**
-	 * @see org.apache.wicket.pageStore.IDataStore#getData(java.lang.String, int)
-	 */
-	@Override
-	public byte[] getData(final String sessionId, final int id)
-	{
-		byte[] pageData = null;
-		SessionEntry sessionEntry = getSessionEntry(sessionId, false);
-		if (sessionEntry != null)
-		{
-			pageData = sessionEntry.loadPage(id);
-		}
-
-		if (log.isDebugEnabled())
-		{
-			log.debug("Returning data{} for page with id '{}' in session with id '{}'",
-					pageData != null ? "" : "(null)", id, sessionId);
-		}
-		return pageData;
-	}
-
-	/**
-	 * @see org.apache.wicket.pageStore.IDataStore#isReplicated()
-	 */
-	@Override
-	public boolean isReplicated()
-	{
-		return false;
-	}
-
-	/**
-	 * @see org.apache.wicket.pageStore.IDataStore#removeData(java.lang.String, int)
-	 */
-	@Override
-	public void removeData(final String sessionId, final int id)
-	{
-		SessionEntry sessionEntry = getSessionEntry(sessionId, false);
-		if (sessionEntry != null)
-		{
-			if (log.isDebugEnabled())
-			{
-				log.debug("Removing data for page with id '{}' in session with id '{}'", id, sessionId);
-			}
-			sessionEntry.removePage(id);
-		}
-	}
-
-	/**
-	 * @see org.apache.wicket.pageStore.IDataStore#removeData(java.lang.String)
-	 */
-	@Override
-	public void removeData(final String sessionId)
-	{
-		SessionEntry sessionEntry = getSessionEntry(sessionId, false);
-		if (sessionEntry != null)
-		{
-			log.debug("Removing data for pages in session with id '{}'", sessionId);
-			synchronized (sessionEntry)
-			{
-				sessionEntryMap.remove(sessionEntry.sessionId);
-				sessionEntry.unbind();
-			}
-		}
-	}
-
-	/**
-	 * @see org.apache.wicket.pageStore.IDataStore#storeData(java.lang.String, int, byte[])
-	 */
-	@Override
-	public void storeData(final String sessionId, final int id, final byte[] data)
-	{
-		SessionEntry sessionEntry = getSessionEntry(sessionId, true);
-		if (sessionEntry != null)
-		{
-			log.debug("Storing data for page with id '{}' in session with id '{}'", id, sessionId);
-			sessionEntry.savePage(id, data);
-		}
-	}
-
-	/**
-	 * 
-	 * @param sessionId
-	 * @param create
-	 * @return the session entry
-	 */
-	protected SessionEntry getSessionEntry(final String sessionId, final boolean create)
-	{
-		if (!create)
-		{
-			return sessionEntryMap.get(sessionId);
-		}
-
-		SessionEntry entry = new SessionEntry(this, sessionId);
-		SessionEntry existing = sessionEntryMap.putIfAbsent(sessionId, entry);
-		return existing != null ? existing : entry;
-	}
-
-	/**
-	 * Load the index
-	 */
-	@SuppressWarnings("unchecked")
-	private void loadIndex()
-	{
-		File storeFolder = getStoreFolder();
-		File index = new File(storeFolder, INDEX_FILE_NAME);
-		if (index.exists() && index.length() > 0)
-		{
-			try
-			{
-				InputStream stream = new FileInputStream(index);
-				ObjectInputStream ois = new ObjectInputStream(stream);
-				try
-				{
-					Map<String, SessionEntry> map = (Map<String, SessionEntry>)ois.readObject();
-					sessionEntryMap.clear();
-					sessionEntryMap.putAll(map);
-
-					for (Entry<String, SessionEntry> entry : sessionEntryMap.entrySet())
-					{
-						// initialize the diskPageStore reference
-						SessionEntry sessionEntry = entry.getValue();
-						sessionEntry.diskDataStore = this;
-					}
-				} finally {
-					stream.close();
-					ois.close();
-				}
-			}
-			catch (Exception e)
-			{
-				log.error("Couldn't load DiskDataStore index from file " + index + ".", e);
-			}
-		}
-		Files.remove(index);
-	}
-
-	/**
-	 * 
-	 */
-	private void saveIndex()
-	{
-		File storeFolder = getStoreFolder();
-		if (storeFolder.exists())
-		{
-			File index = new File(storeFolder, INDEX_FILE_NAME);
-			Files.remove(index);
-			try
-			{
-				OutputStream stream = new FileOutputStream(index);
-				ObjectOutputStream oos = new ObjectOutputStream(stream);
-				try
-				{
-					Map<String, SessionEntry> map = new HashMap<>(sessionEntryMap.size());
-					for (Entry<String, SessionEntry> e : sessionEntryMap.entrySet())
-					{
-						if (e.getValue().unbound == false)
-						{
-							map.put(e.getKey(), e.getValue());
-						}
-					}
-					oos.writeObject(map);
-				} finally {
-					stream.close();
-					oos.close();
-				}
-			}
-			catch (Exception e)
-			{
-				log.error("Couldn't write DiskDataStore index to file " + index + ".", e);
-			}
-		}
-	}
-
-	/**
-	 * 
-	 */
-	protected static class SessionEntry implements Serializable
-	{
-		private static final long serialVersionUID = 1L;
-
-		private final String sessionId;
-		private transient DiskDataStore diskDataStore;
-		private String fileName;
-		private PageWindowManager manager;
-		private boolean unbound = false;
-
-		protected SessionEntry(DiskDataStore diskDataStore, String sessionId)
-		{
-			this.diskDataStore = diskDataStore;
-			this.sessionId = sessionId;
-		}
-
-		public PageWindowManager getManager()
-		{
-			if (manager == null)
-			{
-				manager = new PageWindowManager(diskDataStore.maxSizePerPageSession.bytes());
-			}
-			return manager;
-		}
-
-		private String getFileName()
-		{
-			if (fileName == null)
-			{
-				fileName = diskDataStore.getSessionFileName(sessionId, true);
-			}
-			return fileName;
-		}
-
-		/**
-		 * @return session id
-		 */
-		public String getSessionId()
-		{
-			return sessionId;
-		}
-
-		/**
-		 * Saves the serialized page to appropriate file.
-		 * 
-		 * @param pageId
-		 * @param data
-		 */
-		public synchronized void savePage(int pageId, byte data[])
-		{
-			if (unbound)
-			{
-				return;
-			}
-			// only save page that has some data
-			if (data != null)
-			{
-				// allocate window for page
-				PageWindow window = getManager().createPageWindow(pageId, data.length);
-
-				FileChannel channel = getFileChannel(true);
-				if (channel != null)
-				{
-					try
-					{
-						// write the content
-						channel.write(ByteBuffer.wrap(data), window.getFilePartOffset());
-					}
-					catch (IOException e)
-					{
-						log.error("Error writing to a channel " + channel, e);
-					}
-					finally
-					{
-						IOUtils.closeQuietly(channel);
-					}
-				}
-				else
-				{
-					log.warn(
-						"Cannot save page with id '{}' because the data file cannot be opened.",
-						pageId);
-				}
-			}
-		}
-
-		/**
-		 * Removes the page from pagemap file.
-		 * 
-		 * @param pageId
-		 */
-		public synchronized void removePage(int pageId)
-		{
-			if (unbound)
-			{
-				return;
-			}
-			getManager().removePage(pageId);
-		}
-
-		/**
-		 * Loads the part of pagemap file specified by the given PageWindow.
-		 * 
-		 * @param window
-		 * @return serialized page data
-		 */
-		public byte[] loadPage(PageWindow window)
-		{
-			byte[] result = null;
-			FileChannel channel = getFileChannel(false);
-			if (channel != null)
-			{
-				ByteBuffer buffer = ByteBuffer.allocate(window.getFilePartSize());
-				try
-				{
-					channel.read(buffer, window.getFilePartOffset());
-					if (buffer.hasArray())
-					{
-						result = buffer.array();
-					}
-				}
-				catch (IOException e)
-				{
-					log.error("Error reading from file channel " + channel, e);
-				}
-				finally
-				{
-					IOUtils.closeQuietly(channel);
-				}
-			}
-			return result;
-		}
-
-		private FileChannel getFileChannel(boolean create)
-		{
-			FileChannel channel = null;
-			File file = new File(getFileName());
-			if (create || file.exists())
-			{
-				String mode = create ? "rw" : "r";
-				try
-				{
-					RandomAccessFile randomAccessFile = new RandomAccessFile(file, mode);
-					channel = randomAccessFile.getChannel();
-				}
-				catch (FileNotFoundException fnfx)
-				{
-					// can happen if the file is locked. WICKET-4176
-					log.error(fnfx.getMessage(), fnfx);
-				}
-			}
-			return channel;
-		}
-
-		/**
-		 * Loads the specified page data.
-		 * 
-		 * @param id
-		 * @return page data or null if the page is no longer in pagemap file
-		 */
-		public synchronized byte[] loadPage(int id)
-		{
-			if (unbound)
-			{
-				return null;
-			}
-			byte[] result = null;
-			PageWindow window = getManager().getPageWindow(id);
-			if (window != null)
-			{
-				result = loadPage(window);
-			}
-			return result;
-		}
-
-		/**
-		 * Deletes all files for this session.
-		 */
-		public synchronized void unbind()
-		{
-			File sessionFolder = diskDataStore.getSessionFolder(sessionId, false);
-			if (sessionFolder.exists())
-			{
-				Files.removeFolder(sessionFolder);
-				cleanup(sessionFolder);
-			}
-			unbound = true;
-		}
-
-		/**
-		 * deletes the sessionFolder's parent and grandparent, if (and only if) they are empty.
-		 *
-		 * @see #createPathFrom(String sessionId)
-		 * @param sessionFolder
-		 *            must not be null
-		 */
-		private void cleanup(final File sessionFolder)
-		{
-			File high = sessionFolder.getParentFile();
-			if (high != null && high.list().length == 0)
-			{
-				if (Files.removeFolder(high))
-				{
-					File low = high.getParentFile();
-					if (low != null && low.list().length == 0)
-					{
-						Files.removeFolder(low);
-					}
-				}
-			}
-		}
-	}
-
-	/**
-	 * Returns the file name for specified session. If the session folder (folder that contains the
-	 * file) does not exist and createSessionFolder is true, the folder will be created.
-	 * 
-	 * @param sessionId
-	 * @param createSessionFolder
-	 * @return file name for pagemap
-	 */
-	private String getSessionFileName(String sessionId, boolean createSessionFolder)
-	{
-		File sessionFolder = getSessionFolder(sessionId, createSessionFolder);
-		return new File(sessionFolder, "data").getAbsolutePath();
-	}
-
-	/**
-	 * This folder contains sub-folders named as the session id for which they hold the data.
-	 * 
-	 * @return the folder where the pages are stored
-	 */
-	protected File getStoreFolder()
-	{
-		return new File(fileStoreFolder, applicationName + "-filestore");
-	}
-
-	/**
-	 * Returns the folder for the specified sessions. If the folder doesn't exist and the create
-	 * flag is set, the folder will be created.
-	 * 
-	 * @param sessionId
-	 * @param create
-	 * @return folder used to store session data
-	 */
-	protected File getSessionFolder(String sessionId, final boolean create)
-	{
-		File storeFolder = getStoreFolder();
-
-		sessionId = sessionId.replace('*', '_');
-		sessionId = sessionId.replace('/', '_');
-		sessionId = sessionId.replace(':', '_');
-
-		sessionId = createPathFrom(sessionId);
-
-		File sessionFolder = new File(storeFolder, sessionId);
-		if (create && sessionFolder.exists() == false)
-		{
-			Files.mkdirs(sessionFolder);
-		}
-		return sessionFolder;
-	}
-
-	/**
-	 * creates a three-level path from the sessionId in the format 0000/0000/<sessionId>. The two
-	 * prefixing directories are created from the sessionId's hashcode and thus, should be well
-	 * distributed.
-	 *
-	 * This is used to avoid problems with Filesystems allowing no more than 32k entries in a
-	 * directory.
-	 *
-	 * Note that the prefix paths are created from Integers and not guaranteed to be four chars
-	 * long.
-	 *
-	 * @param sessionId
-	 *      must not be null
-	 * @return path in the form 0000/0000/sessionId
-	 */
-	private String createPathFrom(final String sessionId)
-	{
-		int sessionIdHashCode = sessionId.hashCode();
-		if (sessionIdHashCode == Integer.MIN_VALUE) {
-			// Math.abs(MIN_VALUE) == MIN_VALUE, so avoid it
-			sessionIdHashCode += 1;
-		}
-		int hash = Math.abs(sessionIdHashCode);
-		String low = String.valueOf(hash % 9973);
-		String high = String.valueOf((hash / 9973) % 9973);
-		StringBuilder bs = new StringBuilder(sessionId.length() + 10);
-		bs.append(low);
-		bs.append(File.separator);
-		bs.append(high);
-		bs.append(File.separator);
-		bs.append(sessionId);
-
-		return bs.toString();
-	}
-
-	@Override
-	public boolean canBeAsynchronous()
-	{
-		return true;
-	}
-}

http://git-wip-us.apache.org/repos/asf/wicket/blob/c43d3a33/wicket-core/src/main/java/org/apache/wicket/pageStore/DiskPageStore.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/pageStore/DiskPageStore.java b/wicket-core/src/main/java/org/apache/wicket/pageStore/DiskPageStore.java
new file mode 100644
index 0000000..d3726f0
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/pageStore/DiskPageStore.java
@@ -0,0 +1,710 @@
+/*
+ * 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.pageStore;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.io.Serializable;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import javax.servlet.http.HttpSessionBindingEvent;
+import javax.servlet.http.HttpSessionBindingListener;
+
+import org.apache.wicket.Session;
+import org.apache.wicket.WicketRuntimeException;
+import org.apache.wicket.page.IManageablePage;
+import org.apache.wicket.pageStore.disk.NestedFolders;
+import org.apache.wicket.pageStore.disk.PageWindowManager;
+import org.apache.wicket.pageStore.disk.PageWindowManager.FileWindow;
+import org.apache.wicket.protocol.http.PageExpiredException;
+import org.apache.wicket.serialize.ISerializer;
+import org.apache.wicket.util.file.Files;
+import org.apache.wicket.util.io.IOUtils;
+import org.apache.wicket.util.lang.Args;
+import org.apache.wicket.util.lang.Bytes;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A storage of pages on disk.
+ * <p>
+ * Implementation note: {@link DiskPageStore} writes pages into a single file, appending new pages while overwriting the oldest pages.
+ * Since Ajax requests do not change the id of a page, {@link DiskPageStore} offers an optimization to overwrite the most recently written
+ * page, if it has the same id as a new page to write.<p>
+ * However this does not help in case of alternating requests between multiple browser windows: In this case requests are processed for
+ * different page ids and the oldests pages are constantly overwritten (this can easily happen with Ajax timers on one or more pages).
+ * This leads to pages with identical id superfluously kept in the file, while older pages are prematurely expelled.
+ * Any following request to these older pages will then fail {@link PageExpiredException}.   
+ */
+public class DiskPageStore implements IPersistentPageStore
+{
+	private static final Logger log = LoggerFactory.getLogger(DiskPageStore.class);
+
+	private static final String KEY = "wicket:DiskPageStore";
+
+	private static final String INDEX_FILE_NAME = "DiskPageStoreIndex";
+
+	/**
+	 * A cache that holds all page stores.
+	 */
+	private static final ConcurrentMap<String, DiskPageStore> DISK_STORES = new ConcurrentHashMap<>();
+
+	private final String applicationName;
+
+	private final ISerializer serializer;
+
+	private final Bytes maxSizePerSession;
+
+	private final NestedFolders folders;
+
+	private final ConcurrentMap<String, DiskData> diskDatas;
+
+	/**
+	 * Create a store that supports {@link SerializedPage}s only.
+	 * 
+	 * @param applicationName
+	 *            name of application
+	 * @param fileStoreFolder
+	 *            folder to store to
+	 * @param maxSizePerSession
+	 *            maximum size per session
+	 * 
+	 * @see SerializingPageStore
+	 */
+	public DiskPageStore(String applicationName, File fileStoreFolder, Bytes maxSizePerSession)
+	{
+		this(applicationName, fileStoreFolder, maxSizePerSession, null);
+	}
+
+	/**
+	 * Create a store to disk.
+	 * 
+	 * @param applicationName
+	 *            name of application
+	 * @param fileStoreFolder
+	 *            folder to store to
+	 * @param maxSizePerSession
+	 *            maximum size per session
+	 * @param serializer
+	 *            for serialization of pages
+	 */
+	public DiskPageStore(String applicationName, File fileStoreFolder, Bytes maxSizePerSession,
+		ISerializer serializer)
+	{
+		this.applicationName = Args.notNull(applicationName, "applicationName");
+		this.folders = new NestedFolders(new File(fileStoreFolder, applicationName + "-filestore"));
+		this.maxSizePerSession = Args.notNull(maxSizePerSession, "maxSizePerSession");
+		this.serializer = serializer; // optional
+
+		this.diskDatas = new ConcurrentHashMap<>();
+
+		try
+		{
+			if (folders.getBase().exists() || folders.getBase().mkdirs())
+			{
+				loadIndex();
+			}
+			else
+			{
+				log.warn("Cannot create file store folder for some reason.");
+			}
+		}
+		catch (SecurityException e)
+		{
+			throw new WicketRuntimeException(
+				"SecurityException occurred while creating DiskPageStore. Consider using a non-disk based IPageStore implementation. "
+					+ "See org.apache.wicket.Application.setPageManagerProvider(IPageManagerProvider)",
+				e);
+		}
+
+		if (DISK_STORES.containsKey(applicationName))
+		{
+			throw new IllegalStateException(
+				"Store for application with key '" + applicationName + "' already exists.");
+		}
+		DISK_STORES.put(applicationName, this);
+	}
+
+	@Override
+	public void destroy()
+	{
+		log.debug("Destroying...");
+		saveIndex();
+
+		DISK_STORES.remove(applicationName);
+
+		log.debug("Destroyed.");
+	}
+
+	@Override
+	public boolean canBeAsynchronous(IPageContext context)
+	{
+		// session attribute must be added here *before* any asynchronous calls
+		// when session is no longer available
+		getSessionAttribute(context, true);
+
+		return true;
+	}
+
+	@Override
+	public IManageablePage getPage(IPageContext context, int id)
+	{
+		IManageablePage page = null;
+
+		DiskData diskData = getDiskData(context, false);
+		if (diskData != null)
+		{
+			byte[] data = diskData.loadPage(id);
+			if (data != null)
+			{
+				if (serializer == null)
+				{
+					page = new SerializedPage(id, "unknown", data);
+				}
+				else
+				{
+					page = (IManageablePage)serializer.deserialize(data);
+				}
+
+				if (log.isDebugEnabled())
+				{
+					log.debug("Returning page with id '{}' in session with id '{}'", id, context.getSessionId());
+				}
+			}
+		}
+
+		return page;
+	}
+
+	@Override
+	public void removePage(IPageContext context, IManageablePage page)
+	{
+		DiskData diskData = getDiskData(context, false);
+		if (diskData != null)
+		{
+			if (log.isDebugEnabled())
+			{
+				log.debug("Removing page with id '{}' in session with id '{}'", page.getPageId(),
+					context.getSessionId());
+			}
+			diskData.removeData(page.getPageId());
+		}
+	}
+
+	@Override
+	public void removeAllPages(IPageContext context)
+	{
+		DiskData diskData = getDiskData(context, false);
+		if (diskData != null)
+		{
+			removeDiskData(diskData);
+		}
+	}
+
+	protected void removeDiskData(DiskData diskData)
+	{
+		synchronized (diskDatas)
+		{
+			diskDatas.remove(diskData.sessionIdentifier);
+			diskData.unbind();
+		}
+	}
+
+	/**
+	 * Supports {@link SerializedPage}s too - for this to work the delegating {@link IPageStore}
+	 * must use the same {@link ISerializer} as this one.
+	 */
+	@Override
+	public void addPage(IPageContext context, IManageablePage page)
+	{
+		DiskData diskData = getDiskData(context, true);
+		if (diskData != null)
+		{
+			log.debug("Storing data for page with id '{}' in session with id '{}'",
+				page.getPageId(), context.getSessionId());
+
+			byte[] data;
+			String type;
+			if (page instanceof SerializedPage)
+			{
+				data = ((SerializedPage)page).getData();
+				type = ((SerializedPage)page).getPageType();
+			}
+			else
+			{
+				if (serializer == null)
+				{
+					throw new WicketRuntimeException(
+						"DiskPageStore not configured for serialization");
+				}
+				data = serializer.serialize(page);
+				type = page.getClass().getName();
+			}
+
+			diskData.savePage(page.getPageId(), type, data);
+		}
+	}
+
+	/**
+	 * 
+	 * @param context
+	 * @param create
+	 * @return the session entry
+	 */
+	protected DiskData getDiskData(final IPageContext context, final boolean create)
+	{
+		SessionAttribute attribute = getSessionAttribute(context, create);
+
+		if (!create && attribute == null)
+		{
+			return null;
+		}
+
+		return getDiskData(attribute.identifier, create);
+	}
+
+	protected DiskData getDiskData(String sessionIdentifier, boolean create)
+	{
+		if (!create)
+		{
+			return diskDatas.get(sessionIdentifier);
+		}
+
+		DiskData data = new DiskData(this, sessionIdentifier);
+		DiskData existing = diskDatas.putIfAbsent(sessionIdentifier, data);
+		return existing != null ? existing : data;
+	}
+
+	protected SessionAttribute getSessionAttribute(IPageContext context, boolean create)
+	{
+		context.bind();
+
+		SessionAttribute attribute = context.getSessionAttribute(KEY);
+		if (attribute == null && create)
+		{
+			attribute = new SessionAttribute(applicationName, context.getSessionId());
+			context.setSessionAttribute(KEY, attribute);
+		}
+		return attribute;
+	}
+
+	/**
+	 * Load the index
+	 */
+	@SuppressWarnings("unchecked")
+	private void loadIndex()
+	{
+		File storeFolder = folders.getBase();
+
+		File index = new File(storeFolder, INDEX_FILE_NAME);
+		if (index.exists() && index.length() > 0)
+		{
+			try
+			{
+				InputStream stream = new FileInputStream(index);
+				ObjectInputStream ois = new ObjectInputStream(stream);
+				try
+				{
+					diskDatas.clear();
+
+					for (DiskData diskData : (List<DiskData>)ois.readObject())
+					{
+						diskData.pageStore = this;
+						diskDatas.put(diskData.sessionIdentifier, diskData);
+					}
+				}
+				finally
+				{
+					stream.close();
+					ois.close();
+				}
+			}
+			catch (Exception e)
+			{
+				log.error("Couldn't load DiskPageStore index from file " + index + ".", e);
+			}
+		}
+		Files.remove(index);
+	}
+
+	/**
+	 * 
+	 */
+	private void saveIndex()
+	{
+		File storeFolder = folders.getBase();
+		if (storeFolder.exists())
+		{
+			File index = new File(storeFolder, INDEX_FILE_NAME);
+			Files.remove(index);
+			try
+			{
+				OutputStream stream = new FileOutputStream(index);
+				ObjectOutputStream oos = new ObjectOutputStream(stream);
+				try
+				{
+					List<DiskData> list = new ArrayList<>(diskDatas.size());
+					for (DiskData diskData : diskDatas.values())
+					{
+						if (diskData.sessionIdentifier != null)
+						{
+							list.add(diskData);
+						}
+					}
+					oos.writeObject(list);
+				}
+				finally
+				{
+					stream.close();
+					oos.close();
+				}
+			}
+			catch (Exception e)
+			{
+				log.error("Couldn't write DiskPageStore index to file " + index + ".", e);
+			}
+		}
+	}
+
+	@Override
+	public Set<String> getContextIdentifiers()
+	{
+		return Collections.unmodifiableSet(diskDatas.keySet());
+	}
+
+	/**
+	 * 
+	 * @param session
+	 *            key
+	 * @return a list of the last N page windows
+	 */
+	@Override
+	public List<IPersistedPage> getPersistentPages(String sessionIdentifier)
+	{
+		List<IPersistedPage> pages = new ArrayList<>();
+
+		DiskData diskData = getDiskData(sessionIdentifier, false);
+		if (diskData != null)
+		{
+			PageWindowManager windowManager = diskData.getManager();
+
+			pages.addAll(windowManager.getFileWindows());
+		}
+		return pages;
+	}
+
+	@Override
+	public String getContextIdentifier(IPageContext context)
+	{
+		SessionAttribute sessionAttribute = getSessionAttribute(context, true);
+
+		return sessionAttribute.identifier;
+	}
+
+	@Override
+	public Bytes getTotalSize()
+	{
+		long size = 0;
+
+		synchronized (diskDatas)
+		{
+			for (DiskData diskData : diskDatas.values())
+			{
+				size = size + diskData.size();
+			}
+		}
+
+		return Bytes.bytes(size);
+	}
+
+	/**
+	 * Data held on disk.
+	 */
+	protected static class DiskData implements Serializable
+	{
+		private static final long serialVersionUID = 1L;
+
+		private transient DiskPageStore pageStore;
+
+		private transient String fileName;
+
+		private String sessionIdentifier;
+
+		private PageWindowManager manager;
+
+		protected DiskData(DiskPageStore pageStore, String sessionIdentifier)
+		{
+			this.pageStore = pageStore;
+
+			this.sessionIdentifier = sessionIdentifier;
+		}
+
+		public long size()
+		{
+			return manager.getTotalSize();
+		}
+
+		public PageWindowManager getManager()
+		{
+			if (manager == null)
+			{
+				manager = new PageWindowManager(pageStore.maxSizePerSession.bytes());
+			}
+			return manager;
+		}
+
+		private String getFileName()
+		{
+			if (fileName == null)
+			{
+				fileName = pageStore.getSessionFileName(sessionIdentifier, true);
+			}
+			return fileName;
+		}
+
+		/**
+		 * @return session id
+		 */
+		public String getKey()
+		{
+			return sessionIdentifier;
+		}
+
+		/**
+		 * Saves the serialized page to appropriate file.
+		 * 
+		 * @param pageId
+		 * @param pageType
+		 * @param data
+		 */
+		public synchronized void savePage(int pageId, String pageType, byte data[])
+		{
+			if (sessionIdentifier == null)
+			{
+				return;
+			}
+
+			// only save page that has some data
+			if (data != null)
+			{
+				// allocate window for page
+				FileWindow window = getManager().createPageWindow(pageId, pageType, data.length);
+
+				FileChannel channel = getFileChannel(true);
+				if (channel != null)
+				{
+					try
+					{
+						// write the content
+						channel.write(ByteBuffer.wrap(data), window.getFilePartOffset());
+					}
+					catch (IOException e)
+					{
+						log.error("Error writing to a channel " + channel, e);
+					}
+					finally
+					{
+						IOUtils.closeQuietly(channel);
+					}
+				}
+				else
+				{
+					log.warn(
+						"Cannot save page with id '{}' because the data file cannot be opened.",
+						pageId);
+				}
+			}
+		}
+
+		/**
+		 * Removes the page from pagemap file.
+		 * 
+		 * @param pageId
+		 */
+		public synchronized void removeData(int pageId)
+		{
+			if (sessionIdentifier == null)
+			{
+				return;
+			}
+
+			getManager().removePage(pageId);
+		}
+
+		/**
+		 * Loads the part of pagemap file specified by the given PageWindow.
+		 * 
+		 * @param window
+		 * @return serialized page data
+		 */
+		public byte[] loadData(FileWindow window)
+		{
+			byte[] result = null;
+			FileChannel channel = getFileChannel(false);
+			if (channel != null)
+			{
+				ByteBuffer buffer = ByteBuffer.allocate(window.getFilePartSize());
+				try
+				{
+					channel.read(buffer, window.getFilePartOffset());
+					if (buffer.hasArray())
+					{
+						result = buffer.array();
+					}
+				}
+				catch (IOException e)
+				{
+					log.error("Error reading from file channel " + channel, e);
+				}
+				finally
+				{
+					IOUtils.closeQuietly(channel);
+				}
+			}
+			return result;
+		}
+
+		private FileChannel getFileChannel(boolean create)
+		{
+			FileChannel channel = null;
+			File file = new File(getFileName());
+			if (create || file.exists())
+			{
+				String mode = create ? "rw" : "r";
+				try
+				{
+					RandomAccessFile randomAccessFile = new RandomAccessFile(file, mode);
+					channel = randomAccessFile.getChannel();
+				}
+				catch (FileNotFoundException fnfx)
+				{
+					// can happen if the file is locked. WICKET-4176
+					log.error(fnfx.getMessage(), fnfx);
+				}
+			}
+			return channel;
+		}
+
+		/**
+		 * Loads the specified page data.
+		 * 
+		 * @param id
+		 * @return page data or null if the page is no longer in pagemap file
+		 */
+		public synchronized byte[] loadPage(int id)
+		{
+			if (sessionIdentifier == null)
+			{
+				return null;
+			}
+
+			FileWindow window = getManager().getPageWindow(id);
+			if (window == null)
+			{
+				return null;
+			}
+
+			return loadData(window);
+		}
+
+		/**
+		 * Deletes all files for this session.
+		 */
+		public synchronized void unbind()
+		{
+			pageStore.folders.remove(sessionIdentifier);
+
+			sessionIdentifier = null;
+		}
+	}
+
+	/**
+	 * Returns the file name for specified session. If the session folder (folder that contains the
+	 * file) does not exist and createSessionFolder is true, the folder will be created.
+	 * 
+	 * @param sessionIdentifier
+	 * @param createSessionFolder
+	 * @return file name for pagemap
+	 */
+	private String getSessionFileName(String sessionIdentifier, boolean createSessionFolder)
+	{
+		File sessionFolder = folders.get(sessionIdentifier, createSessionFolder);
+		return new File(sessionFolder, "data").getAbsolutePath();
+	}
+
+	/**
+	 * Attribute held in session.
+	 */
+	static class SessionAttribute implements Serializable, HttpSessionBindingListener
+	{
+
+		private final String applicationName;
+
+		/**
+		 * The identifier of the session, must not be equal to {@link Session#getId()}, e.g. when
+		 * the container changes the id after authorization.
+		 */
+		public final String identifier;
+
+		public SessionAttribute(String applicationName, String sessionIdentifier)
+		{
+			this.applicationName = Args.notNull(applicationName, "applicationName");
+			this.identifier = Args.notNull(sessionIdentifier, "sessionIdentifier");
+		}
+
+
+		@Override
+		public void valueBound(HttpSessionBindingEvent event)
+		{
+		}
+
+		@Override
+		public void valueUnbound(HttpSessionBindingEvent event)
+		{
+			DiskPageStore store = DISK_STORES.get(applicationName);
+			if (store == null)
+			{
+				log.warn(
+					"Cannot remove data '{}' because disk store for application '{}' is no longer present.",
+					identifier, applicationName);
+			}
+			else
+			{
+				DiskData diskData = store.getDiskData(identifier, false);
+				if (diskData != null)
+				{
+					store.removeDiskData(diskData);
+				}
+			}
+		}
+	}
+}
\ No newline at end of file