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