/*
* 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.lang.ref.SoftReference;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentSkipListMap;
import org.apache.wicket.page.IManageablePage;
import org.apache.wicket.serialize.ISerializer;
import org.apache.wicket.util.lang.Args;
/**
* A page store that uses a SecondLevelPageCache with the last N used page instances
* per session.
*
* <strong>Note</strong>: the size of the cache depends on the {@code cacheSize} constructor
* parameter multiplied by the number of the active http sessions.
*
* It depends on the application use cases but usually a reasonable value of
* {@code cacheSize} would be just a few pages (2-3). If the application don't expect many
* active http sessions and the work flow involves usage of the browser/application history
* then the {@code cacheSize} value may be increased to a bigger value.
*/
public class PerSessionPageStore extends AbstractCachingPageStore<IManageablePage>
{
/**
* Constructor.
*
* @param pageSerializer
* the {@link org.apache.wicket.serialize.ISerializer} that will be used to convert pages from/to byte arrays
* @param dataStore
* the {@link org.apache.wicket.pageStore.IDataStore} that actually stores the pages
* @param cacheSize
* the number of pages to cache in memory before passing them to
* {@link org.apache.wicket.pageStore.IDataStore#storeData(String, int, byte[])}
*/
public PerSessionPageStore(final ISerializer pageSerializer, final IDataStore dataStore,
final int cacheSize)
{
super(pageSerializer, dataStore, new PagesCache(cacheSize));
}
@Override
public IManageablePage convertToPage(final Object object)
{
if (object == null)
{
return null;
}
else if (object instanceof IManageablePage)
{
return (IManageablePage)object;
}
String type = object.getClass().getName();
throw new IllegalArgumentException("Unknown object type: " + type);
}
/**
* An implementation of SecondLevelPageCache that stores the last used N live page instances
* per http session.
*/
protected static class PagesCache implements SecondLevelPageCache<String, Integer, IManageablePage>
{
/**
* Helper class used to compare the page entries in the cache by their
* access time
*/
private static class PageValue
{
/**
* The id of the cached page
*/
private final int pageId;
/**
* The last time this page has been used/accessed.
*/
private long accessTime;
private PageValue(IManageablePage page)
{
this(page.getPageId());
}
private PageValue(int pageId)
{
this.pageId = pageId;
touch();
}
/**
* Updates the access time with the current time
*/
private void touch()
{
accessTime = System.nanoTime();
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PageValue pageValue = (PageValue) o;
return pageId == pageValue.pageId;
}
@Override
public int hashCode()
{
return pageId;
}
}
private static class PageComparator implements Comparator<PageValue>
{
@Override
public int compare(PageValue p1, PageValue p2)
{
return Long.compare(p1.accessTime, p2.accessTime);
}
}
private final int maxEntriesPerSession;
private final ConcurrentMap<String, SoftReference<ConcurrentSkipListMap<PageValue, IManageablePage>>> cache;
/**
* Constructor.
*
* @param maxEntriesPerSession
* The number of cache entries per session
*/
public PagesCache(final int maxEntriesPerSession)
{
this.maxEntriesPerSession = maxEntriesPerSession;
cache = new ConcurrentHashMap<>();
}
/**
*
* @param sessionId
* The id of the http session
* @param pageId
* The id of the page to remove from the cache
* @return the removed {@link org.apache.wicket.page.IManageablePage} or <code>null</code> - otherwise
*/
@Override
public IManageablePage removePage(final String sessionId, final Integer pageId)
{
IManageablePage result = null;
if (maxEntriesPerSession > 0)
{
Args.notNull(sessionId, "sessionId");
Args.notNull(pageId, "pageId");
SoftReference<ConcurrentSkipListMap<PageValue, IManageablePage>> pagesPerSession = cache.get(sessionId);
if (pagesPerSession != null)
{
ConcurrentMap<PageValue, IManageablePage> pages = pagesPerSession.get();
if (pages != null)
{
PageValue sample = new PageValue(pageId);
Iterator<Map.Entry<PageValue, IManageablePage>> iterator = pages.entrySet().iterator();
while (iterator.hasNext())
{
Map.Entry<PageValue, IManageablePage> entry = iterator.next();
if (sample.equals(entry.getKey()))
{
result = entry.getValue();
iterator.remove();
break;
}
}
}
}
}
return result;
}
/**
* Removes all {@link org.apache.wicket.page.IManageablePage}s for the session
* with <code>sessionId</code> from the cache.
*
* @param sessionId
* The id of the expired http session
*/
@Override
public void removePages(String sessionId)
{
Args.notNull(sessionId, "sessionId");
if (maxEntriesPerSession > 0)
{
cache.remove(sessionId);
}
}
/**
* Returns a {@link org.apache.wicket.page.IManageablePage} 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
* The id of the http session
* @param pageId
* The id of the page to find
* @return the found serialized page or <code>null</code> when not found
*/
@Override
public IManageablePage getPage(String sessionId, Integer pageId)
{
IManageablePage result = null;
if (maxEntriesPerSession > 0)
{
Args.notNull(sessionId, "sessionId");
Args.notNull(pageId, "pageId");
SoftReference<ConcurrentSkipListMap<PageValue, IManageablePage>> pagesPerSession = cache.get(sessionId);
if (pagesPerSession != null)
{
ConcurrentSkipListMap<PageValue, IManageablePage> pages = pagesPerSession.get();
if (pages != null)
{
PageValue sample = new PageValue(pageId);
for (Map.Entry<PageValue, IManageablePage> entry : pages.entrySet())
{
if (sample.equals(entry.getKey()))
{
// touch the entry
entry.getKey().touch();
result = entry.getValue();
break;
}
}
}
}
}
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, IManageablePage page)
{
if (maxEntriesPerSession > 0)
{
Args.notNull(sessionId, "sessionId");
Args.notNull(pageId, "pageId");
SoftReference<ConcurrentSkipListMap<PageValue, IManageablePage>> pagesPerSession = cache.get(sessionId);
if (pagesPerSession == null)
{
ConcurrentSkipListMap<PageValue, IManageablePage> pages = new ConcurrentSkipListMap<>(new PageComparator());
pagesPerSession = new SoftReference<>(pages);
SoftReference<ConcurrentSkipListMap<PageValue, IManageablePage>> old = cache.putIfAbsent(sessionId, pagesPerSession);
if (old != null)
{
pagesPerSession = old;
}
}
ConcurrentSkipListMap<PageValue, IManageablePage> pages = pagesPerSession.get();
if (pages == null)
{
pages = new ConcurrentSkipListMap<>();
pagesPerSession = new SoftReference<>(pages);
SoftReference<ConcurrentSkipListMap<PageValue, IManageablePage>> old = cache.putIfAbsent(sessionId, pagesPerSession);
if (old != null)
{
pages = old.get();
}
}
if (pages != null)
{
removePage(sessionId, pageId);
PageValue pv = new PageValue(page);
pages.put(pv, page);
while (pages.size() > maxEntriesPerSession)
{
pages.pollFirstEntry();
}
}
}
}
@Override
public void destroy()
{
cache.clear();
}
}
@Override
public boolean canBeAsynchronous()
{
return false; // NOTE: not analyzed neither tested yet, this page store being wrapped by asynchronous one
}
}