org.apache.wicket.pageStore.DiskDataStore Maven / Gradle / Ivy
Go to download
A module that creates a .jar from the classes in wicket, wicket-util and wicket-request modules in order
to create a valid OSGi bundle of the wicket framework.
/*
* 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 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)
{
if (log.isDebugEnabled())
{
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 map = (Map)ois.readObject();
sessionEntryMap.clear();
sessionEntryMap.putAll(map);
for (Entry 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 map = new HashMap(
sessionEntryMap.size());
for (Entry 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/. 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;
}
}