You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@wicket.apache.org by jd...@apache.org on 2009/12/19 22:15:37 UTC

svn commit: r892505 [2/2] - in /wicket/trunk/wicket/src: main/java/org/apache/wicket/ main/java/org/apache/wicket/ng/ main/java/org/apache/wicket/ng/markup/html/ main/java/org/apache/wicket/ng/mock/ main/java/org/apache/wicket/ng/page/persistent/ main/...

Added: wicket/trunk/wicket/src/main/java/org/apache/wicket/pageStore/PersistentPageManager.java
URL: http://svn.apache.org/viewvc/wicket/trunk/wicket/src/main/java/org/apache/wicket/pageStore/PersistentPageManager.java?rev=892505&view=auto
==============================================================================
--- wicket/trunk/wicket/src/main/java/org/apache/wicket/pageStore/PersistentPageManager.java (added)
+++ wicket/trunk/wicket/src/main/java/org/apache/wicket/pageStore/PersistentPageManager.java Sat Dec 19 21:15:34 2009
@@ -0,0 +1,365 @@
+/*
+ * 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.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.wicket.ng.page.IManageablePage;
+
+/**
+ * 
+ */
+public class PersistentPageManager extends AbstractPageManager
+{
+	private static Map<String, PersistentPageManager> managers = new ConcurrentHashMap<String, PersistentPageManager>();
+
+	private final IPageStore pageStore;
+
+	private final String applicationName;
+
+	/**
+	 * Construct.
+	 * 
+	 * @param applicationName
+	 * @param pageStore
+	 * @param context
+	 */
+	public PersistentPageManager(final String applicationName, final IPageStore pageStore,
+		final IPageManagerContext context)
+	{
+		super(context);
+
+		this.applicationName = applicationName;
+		this.pageStore = pageStore;
+
+		managers.put(applicationName, this);
+	}
+
+	/**
+	 * Represents entry for single session. This is stored as session attribute and caches pages
+	 * between requests.
+	 * 
+	 * @author Matej Knopp
+	 */
+	private static class SessionEntry implements Serializable
+	{
+		private static final long serialVersionUID = 1L;
+
+		private final String applicationName;
+
+		private final String sessionId;
+
+		private transient List<IManageablePage> pages;
+		private transient List<Object> afterReadObject;
+
+		/**
+		 * Construct.
+		 * 
+		 * @param applicationName
+		 * @param sessionId
+		 */
+		public SessionEntry(String applicationName, String sessionId)
+		{
+			this.applicationName = applicationName;
+			this.sessionId = sessionId;
+		}
+
+		/**
+		 * 
+		 * @return page store
+		 */
+		private IPageStore getPageStore()
+		{
+			PersistentPageManager manager = managers.get(applicationName);
+			if (manager == null)
+			{
+				throw new IllegalStateException("PageManager for application " + applicationName +
+					" not registered.");
+			}
+			return manager.pageStore;
+		}
+
+		/**
+		 * 
+		 * @param id
+		 * @return null, if not found
+		 */
+		private IManageablePage findPage(int id)
+		{
+			for (IManageablePage p : pages)
+			{
+				if (p.getPageId() == id)
+				{
+					return p;
+				}
+			}
+			return null;
+		}
+
+		/**
+		 * Add the page to cached pages if page with same id is not already there
+		 * 
+		 * @param page
+		 */
+		private void addPage(IManageablePage page)
+		{
+			if (page != null)
+			{
+				if (findPage(page.getPageId()) != null)
+				{
+					return;
+				}
+			}
+			pages.add(page);
+		}
+
+		/**
+		 * If the pages are stored in temporary state (after deserialization) this method convert
+		 * them to list of "real" pages
+		 */
+		private void convertAfterReadObjects()
+		{
+			if (pages == null)
+			{
+				pages = new ArrayList<IManageablePage>();
+			}
+
+			for (Object o : afterReadObject)
+			{
+				IManageablePage page = getPageStore().convertToPage(o);
+				addPage(page);
+			}
+
+			afterReadObject = null;
+		}
+
+		/**
+		 * 
+		 * @param id
+		 * @return manageable page
+		 */
+		public synchronized IManageablePage getPage(int id)
+		{
+			// check if pages are in deserialized state
+			if (afterReadObject != null && afterReadObject.isEmpty() == false)
+			{
+				convertAfterReadObjects();
+			}
+
+			// try to find page with same id
+			if (pages != null)
+			{
+				IManageablePage page = findPage(id);
+				if (page != null)
+				{
+					return page;
+				}
+			}
+
+			// not found, ask pagestore for the page
+			return getPageStore().getPage(sessionId, id);
+		}
+
+		/**
+		 * set the list of pages to remember after the request
+		 * 
+		 * @param pages
+		 */
+		public synchronized void setPages(final List<IManageablePage> pages)
+		{
+			this.pages = new ArrayList<IManageablePage>(pages);
+			afterReadObject = null;
+		}
+
+		/**
+		 * 
+		 * @param s
+		 * @throws IOException
+		 */
+		private void writeObject(final ObjectOutputStream s) throws IOException
+		{
+			s.defaultWriteObject();
+
+			// prepare for serialization and store the pages
+			List<Serializable> l = new ArrayList<Serializable>();
+			for (IManageablePage p : pages)
+			{
+				l.add(getPageStore().prepareForSerialization(sessionId, p));
+			}
+			s.writeObject(l);
+		}
+
+		/**
+		 * 
+		 * @param s
+		 * @throws IOException
+		 * @throws ClassNotFoundException
+		 */
+		@SuppressWarnings("unchecked")
+		private void readObject(final ObjectInputStream s) throws IOException,
+			ClassNotFoundException
+		{
+			s.defaultReadObject();
+
+			afterReadObject = new ArrayList<Object>();
+
+			List<Serializable> l = (List<Serializable>)s.readObject();
+
+			// convert to temporary state after deserialization (will need to be processed
+			// by convertAfterReadObject before the pages can be accessed)
+			for (Serializable ser : l)
+			{
+				afterReadObject.add(getPageStore().restoreAfterSerialization(ser));
+			}
+		}
+	}
+
+	/**
+	 * {@link RequestAdapter} for {@link PersistentPageManager}
+	 * 
+	 * @author Matej Knopp
+	 */
+	protected class PersitentRequestAdapter extends RequestAdapter
+	{
+		private static final String ATTRIBUTE_NAME = "wicket:persistentPageManagerData";
+
+		/**
+		 * Construct.
+		 * 
+		 * @param context
+		 */
+		public PersitentRequestAdapter(IPageManagerContext context)
+		{
+			super(context);
+		}
+
+		/**
+		 * @see org.apache.wicket.pageStore.RequestAdapter#getPage(int)
+		 */
+		@Override
+		protected IManageablePage getPage(int id)
+		{
+			// try to get session entry for this session
+			SessionEntry entry = getSessionEntry(false);
+
+			if (entry != null)
+			{
+				return entry.getPage(id);
+			}
+			else
+			{
+				return null;
+			}
+		}
+
+		/**
+		 * 
+		 * @param create
+		 * @return Session Entry
+		 */
+		private SessionEntry getSessionEntry(boolean create)
+		{
+			SessionEntry entry = (SessionEntry)getSessionAttribute(ATTRIBUTE_NAME);
+			if (entry == null && create)
+			{
+				bind();
+				entry = new SessionEntry(applicationName, getSessionId());
+			}
+			if (entry != null)
+			{
+				synchronized (entry)
+				{
+					setSessionAttribute(ATTRIBUTE_NAME, null);
+					setSessionAttribute(ATTRIBUTE_NAME, entry);
+				}
+			}
+			return entry;
+		}
+
+		/**
+		 * @see org.apache.wicket.pageStore.RequestAdapter#newSessionCreated()
+		 */
+		@Override
+		protected void newSessionCreated()
+		{
+			// if the session is not temporary bind a session entry to it
+			if (getSessionId() != null)
+			{
+				getSessionEntry(true);
+			}
+		}
+
+		/**
+		 * @see org.apache.wicket.pageStore.RequestAdapter#storeTouchedPages(java.util.List)
+		 */
+		@Override
+		protected void storeTouchedPages(final List<IManageablePage> touchedPages)
+		{
+			if (!touchedPages.isEmpty())
+			{
+				SessionEntry entry = getSessionEntry(true);
+				entry.setPages(touchedPages);
+				for (IManageablePage page : touchedPages)
+				{
+					pageStore.storePage(getSessionId(), page);
+				}
+			}
+		}
+	}
+
+	/**
+	 * @see org.apache.wicket.pageStore.AbstractPageManager#newRequestAdapter(org.apache.wicket.pageStore.IPageManagerContext)
+	 */
+	@Override
+	protected RequestAdapter newRequestAdapter(IPageManagerContext context)
+	{
+		return new PersitentRequestAdapter(context);
+	}
+
+	/**
+	 * @see org.apache.wicket.pageStore.AbstractPageManager#supportsVersioning()
+	 */
+	@Override
+	public boolean supportsVersioning()
+	{
+		return true;
+	}
+
+	/**
+	 * @see org.apache.wicket.pageStore.AbstractPageManager#sessionExpired(java.lang.String)
+	 */
+	@Override
+	public void sessionExpired(String sessionId)
+	{
+		pageStore.unbind(sessionId);
+	}
+
+	/**
+	 * @see org.apache.wicket.pageStore.IPageManager#destroy()
+	 */
+	public void destroy()
+	{
+		managers.remove(applicationName);
+	}
+}

Added: wicket/trunk/wicket/src/main/java/org/apache/wicket/pageStore/RequestAdapter.java
URL: http://svn.apache.org/viewvc/wicket/trunk/wicket/src/main/java/org/apache/wicket/pageStore/RequestAdapter.java?rev=892505&view=auto
==============================================================================
--- wicket/trunk/wicket/src/main/java/org/apache/wicket/pageStore/RequestAdapter.java (added)
+++ wicket/trunk/wicket/src/main/java/org/apache/wicket/pageStore/RequestAdapter.java Sat Dec 19 21:15:34 2009
@@ -0,0 +1,190 @@
+/*
+ * 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.util.ArrayList;
+import java.util.List;
+
+import org.apache.wicket.ng.page.IManageablePage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Request scoped helper class for {@link IPageManager}.
+ * 
+ * @author Matej Knopp
+ */
+public abstract class RequestAdapter
+{
+	private static final Logger log = LoggerFactory.getLogger(RequestAdapter.class);
+
+	private final IPageManagerContext context;
+
+	private final List<IManageablePage> touchedPages = new ArrayList<IManageablePage>();
+
+	private final List<IManageablePage> pages = new ArrayList<IManageablePage>();
+
+	/**
+	 * Construct.
+	 * 
+	 * @param context
+	 *            The page manager context
+	 */
+	public RequestAdapter(final IPageManagerContext context)
+	{
+		this.context = context;
+	}
+
+	/**
+	 * Returns the page with specified id. The page is then cached by {@link RequestAdapter} during
+	 * the rest of request processing.
+	 * 
+	 * @param id
+	 * @return page instance or <code>null</code> if the page does not exist.
+	 */
+	protected abstract IManageablePage getPage(int id);
+
+	/**
+	 * Store the list of pages.
+	 * 
+	 * @param touchedPages
+	 */
+	protected abstract void storeTouchedPages(List<IManageablePage> touchedPages);
+
+	/**
+	 * Notification on new session being created.
+	 */
+	protected abstract void newSessionCreated();
+
+	/**
+	 * Bind the session
+	 * 
+	 * @see IPageManagerContext#bind()
+	 */
+	protected void bind()
+	{
+		context.bind();
+	}
+
+	/**
+	 * @see IPageManagerContext#setSessionAttribute(String, Serializable)
+	 * 
+	 * @param key
+	 * @param value
+	 */
+	public void setSessionAttribute(String key, Serializable value)
+	{
+		context.setSessionAttribute(key, value);
+	}
+
+	/**
+	 * @see IPageManagerContext#getSessionAttribute(String)
+	 * 
+	 * @param key
+	 * @return the session attribute
+	 */
+	public Serializable getSessionAttribute(final String key)
+	{
+		return context.getSessionAttribute(key);
+	}
+
+	/**
+	 * @see IPageManagerContext#getSessionId()
+	 * 
+	 * @return session id
+	 */
+	public String getSessionId()
+	{
+		return context.getSessionId();
+	}
+
+	/**
+	 * 
+	 * @param id
+	 * @return null, if not found
+	 */
+	private IManageablePage findPage(final int id)
+	{
+		for (IManageablePage page : pages)
+		{
+			if (page.getPageId() == id)
+			{
+				return page;
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * 
+	 * @param page
+	 */
+	protected void touch(final IManageablePage page)
+	{
+		if (findPage(page.getPageId()) == null)
+		{
+			pages.add(page);
+		}
+
+		for (IManageablePage p : touchedPages)
+		{
+			if (p.getPageId() == page.getPageId())
+			{
+				return;
+			}
+		}
+		touchedPages.add(page);
+	}
+
+	/**
+	 * 
+	 */
+	protected void commitRequest()
+	{
+		for (IManageablePage page : pages)
+		{
+			try
+			{
+				page.detach();
+			}
+			catch (Exception e)
+			{
+				log.error("Error detaching page", e);
+			}
+		}
+
+		// store pages that are not stateless
+		if (touchedPages.isEmpty() == false)
+		{
+			List<IManageablePage> statefulPages = new ArrayList<IManageablePage>(
+				touchedPages.size());
+			for (IManageablePage page : touchedPages)
+			{
+				if (!page.isPageStateless())
+				{
+					statefulPages.add(page);
+				}
+			}
+
+			if (statefulPages.isEmpty() == false)
+			{
+				storeTouchedPages(statefulPages);
+			}
+		}
+	}
+}
\ No newline at end of file

Added: wicket/trunk/wicket/src/main/java/org/apache/wicket/pageStore/page-management.txt
URL: http://svn.apache.org/viewvc/wicket/trunk/wicket/src/main/java/org/apache/wicket/pageStore/page-management.txt?rev=892505&view=auto
==============================================================================
--- wicket/trunk/wicket/src/main/java/org/apache/wicket/pageStore/page-management.txt (added)
+++ wicket/trunk/wicket/src/main/java/org/apache/wicket/pageStore/page-management.txt Sat Dec 19 21:15:34 2009
@@ -0,0 +1,106 @@
+Render Count
+============
+
+Render count is new concept to detect stale page links. 
+Scenario:
+	1. Page contains list of items, each item has "delete" link.
+	2. User clicks delete on first item, but he chooses "open in new tab"
+	3. Page rerenders, but it new tab. The indexes shift and the links that
+	   user sees in first tab no longer match the actual items
+	     (This is not solved by regular versioning, because during page render (when the items are rebuilt)
+	      bumping page version is not possible)
+    4. On first tab user clicks delete on second item
+        
+  This will likely result in deleting invalid item user see stale page
+  
+FIXME: Component hierarchy changes in beforeRender, not during render. At that point we can bump the version.
+       So why is versioning disabled during render in Wicket? What can actually change? RenderCount detects
+       changes that would normally result in version bump but they don't because they happen during render. 
+       What are those changes? 
+       
+UPDATE:
+		Versioning is disabled during page render (i.e. IRedirectRenderer). That also includes onBeforeRender
+		call that can rebuild component hierarchy but it doesn't increment version (otherwise there would be a 
+		new version each time user refreshes a page with listview). This is where render count can detect that
+		page version rendered in one tab is stalled because it was rerendered in another.
+  
+How to detect this?
+
+Page has new variable - render-count. Every time page is rendered and component hierarchy changes render-count 
+increases. The component hierarchy changes *before* render so we know whether to increment render count or not
+before the rendering actually begins.
+
+Every listener-interface link contains the render-count. Every time listener interface request handler is invoked
+there's a check in place that throws StalePageException when the link is stale.
+
+Later in game we can have fancy detection that immediately (matter of seconds) tells user that the page he sees
+become stale (because same page was rendered in another tab/window). This can be achieved by a cookie containing 
+last x page render counts. The cookie is updated on every page render and it's periodically checked for changes.
+
+Basically the difference between render count and page version is that render count can change during page render
+(well, actually in onBeforeRender - because during actual render hierarchy must not change) and is only in
+listener interface links so it never makes it to URL that user sees. On the other hand version changes during
+listener interface actions and is visible in URL. Also version change requires underlying page manager to store
+page snapshot (whereas render is only a property on existing page version).
+
+Q: Will this break master/detail page where each detail link is opened in new window/tab?
+A: No. The detail link either changes component hierarchy (before render) or sets another page as response. So 
+   when the link is clicked the original page never gets rendered thus the links don't become stale.
+   
+
+Page Storage
+============
+
+There will be two page managers. PersistentPageManager and SessionPageManager.
+
+PersistentPageManager
+---------------------
+
+Works like current SecondLevelCacheSessionStore. Has support for versioning, with a slight difference - page version
+is not a separate field. Rather than that page id gets incremented.
+
+SessionPageManager
+------------------
+
+Keeps last N pages in Session as session attributes. Versioning is not supported. Page eviction should take in account
+different pages/tabs. So for example when user goes X pages back PageExpiredException is to be expected. But when user 
+returns to page in different tab he should not gets page expired.
+
+Idea for detecting tabs:
+(this is only relevant for SessionPageManager. PeristentPageManager should keep enough pages/versions to make
+ page expirations rare) 
+
+Current
+  There are no pagemaps so we can't use the solution from current Wicket. Also the solution is not very reliable and
+  under certain circumstances it can lead to various funny things like infinite redirects.
+  
+Possibly better solution
+  Every time page is rendered it checks window.name. If window name is empty it sets it to unique generated value. Page
+  Fires ajax request to server letting it know that PageX has been opened in new tab/window or that it has been opened in
+  existing tab (in that case window.name has already been set to unique value). This request would be very
+  extremely quick and light, merely changing a page manager page property.
+  
+  If page is rerendered it checks the window.name. If it has changed it fires ajax request to server letting it know
+  that the page has either moved to existing tab or been opened in new one.
+  
+
+Locking
+=======
+
+In current wicket version we lock on pagemap. Unfortunately in 99.99% cases this pretty much equals locking on session
+because there is only one pagemap.
+
+Since there will be no pagemaps (actually there will be but only as implementation details of SessionPageManager) we 
+could try locking on individual pages. This introduces several problems:
+
+Assuming users are disciplined and don't pass page instances between pages, rather then that they use PageReferences.
+
+PageA is being processed (current thread has lock on pageA) and it requests page B. If there is no lock on pageB solution 
+is simple - current thread gets lock to pageB as well. 
+
+However if pageB is also being processed, another thread has lock on PageB. PageA would have to wait until pageB is done
+to obtain the lock. But if for some reason PageB requires pageA, this would end in a deadlock.
+
+Perhaps with proper timeouts it would be possible for PageA to obtain PageB instance even though it's locked. This
+is not very safe but I don't really see any other solution. It's a bad idea to put synchronous long-duration tasks to 
+pages anyway.    
\ No newline at end of file

Modified: wicket/trunk/wicket/src/main/java/org/apache/wicket/util/lang/Checks.java
URL: http://svn.apache.org/viewvc/wicket/trunk/wicket/src/main/java/org/apache/wicket/util/lang/Checks.java?rev=892505&r1=892504&r2=892505&view=diff
==============================================================================
--- wicket/trunk/wicket/src/main/java/org/apache/wicket/util/lang/Checks.java (original)
+++ wicket/trunk/wicket/src/main/java/org/apache/wicket/util/lang/Checks.java Sat Dec 19 21:15:34 2009
@@ -18,9 +18,17 @@
 
 import org.apache.wicket.util.string.Strings;
 
+/**
+ * 
+ */
 public class Checks
 {
-	public static void argumentNotNull(Object argument, String name)
+	/**
+	 * 
+	 * @param argument
+	 * @param name
+	 */
+	public static void argumentNotNull(final Object argument, final String name)
 	{
 		if (argument == null)
 		{
@@ -28,7 +36,12 @@
 		}
 	}
 
-	public static void argumentNotEmpty(String argument, String name)
+	/**
+	 * 
+	 * @param argument
+	 * @param name
+	 */
+	public static void argumentNotEmpty(final String argument, final String name)
 	{
 		if (Strings.isEmpty(argument))
 		{

Modified: wicket/trunk/wicket/src/test/java/org/apache/wicket/ng/page/persistent/disk/DiskDataStoreTest.java
URL: http://svn.apache.org/viewvc/wicket/trunk/wicket/src/test/java/org/apache/wicket/ng/page/persistent/disk/DiskDataStoreTest.java?rev=892505&r1=892504&r2=892505&view=diff
==============================================================================
--- wicket/trunk/wicket/src/test/java/org/apache/wicket/ng/page/persistent/disk/DiskDataStoreTest.java (original)
+++ wicket/trunk/wicket/src/test/java/org/apache/wicket/ng/page/persistent/disk/DiskDataStoreTest.java Sat Dec 19 21:15:34 2009
@@ -28,8 +28,9 @@
 
 import junit.framework.TestCase;
 
-import org.apache.wicket.ng.page.persistent.AsynchronousDataStore;
-import org.apache.wicket.ng.page.persistent.IDataStore;
+import org.apache.wicket.pageStore.AsynchronousDataStore;
+import org.apache.wicket.pageStore.DiskDataStore;
+import org.apache.wicket.pageStore.IDataStore;
 import org.apache.wicket.util.lang.Checks;
 
 public class DiskDataStoreTest extends TestCase

Modified: wicket/trunk/wicket/src/test/java/org/apache/wicket/ng/page/persistent/disk/PageWindowManagerTest.java
URL: http://svn.apache.org/viewvc/wicket/trunk/wicket/src/test/java/org/apache/wicket/ng/page/persistent/disk/PageWindowManagerTest.java?rev=892505&r1=892504&r2=892505&view=diff
==============================================================================
--- wicket/trunk/wicket/src/test/java/org/apache/wicket/ng/page/persistent/disk/PageWindowManagerTest.java (original)
+++ wicket/trunk/wicket/src/test/java/org/apache/wicket/ng/page/persistent/disk/PageWindowManagerTest.java Sat Dec 19 21:15:34 2009
@@ -18,7 +18,8 @@
 
 import junit.framework.TestCase;
 
-import org.apache.wicket.ng.page.persistent.disk.PageWindowManager.PageWindow;
+import org.apache.wicket.pageStore.PageWindowManager;
+import org.apache.wicket.pageStore.PageWindowManager.PageWindow;
 
 /**
  * @author Matej Knopp