/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2007-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.caching.grid.spatialindex.store;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.RandomAccessFile;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Properties;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geotools.caching.grid.spatialindex.GridNode;
import org.geotools.caching.spatialindex.Node;
import org.geotools.caching.spatialindex.NodeIdentifier;
import org.geotools.caching.spatialindex.Storage;
import org.geotools.data.DataUtilities;
import org.geotools.feature.SchemaException;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.FeatureType;
/** A storage that stores data in a file on disk.
*
* Create new instances with static factory method <code>DiskStorage.createInstance()</code>
* or <code>DiskStorage.createInstance(PropertySet)</code>
*
* @author Christophe Rousson <christophe.rousson@gmail.com>, Google SoC 2007
*
*
* @source $URL$
*/
public class DiskStorage implements Storage {
public final static String DATA_FILE_PROPERTY = "DiskStorage.DataFile";
public final static String INDEX_FILE_PROPERTY = "DiskStorage.IndexFile";
public final static String PAGE_SIZE_PROPERTY = "DiskStorage.PageSize";
protected static Logger logger = org.geotools.util.logging.Logging.getLogger("org.geotools.caching.spatialindex.store");
private int stats_bytes = 0;
private int stats_n = 0;
private int page_size;
private int nextPage = 0;
private File dataFile; //this is the file that stores the data
private RandomAccessFile data_file;
private FileChannel data_channel;
private File indexFile; //this is the index file that tracks nodes & pages
private TreeSet<Integer> emptyPages; //list of empty pages for reuse
private HashMap<NodeIdentifier, Entry> pageIndex; //page index list
private Collection<FeatureType> featureTypes; //feature types in store
private ReferencedEnvelope bounds; //bounds of store
private DiskStorage(File f, int page_size) throws IOException {
this(f, page_size, new File(f.getCanonicalPath() + ".idx"));
}
private DiskStorage(File f, File index_file) throws IOException {
this(f, 1000, index_file);
}
private DiskStorage(File f, int page_size, File index_file) throws IOException {
this.indexFile = index_file;
this.page_size = page_size;
this.dataFile = f;
this.emptyPages = new TreeSet<Integer>();
this.pageIndex = new HashMap<NodeIdentifier, Entry>();
this.featureTypes = new HashSet<FeatureType>();
if (index_file.exists()) {
try{
initializeFromIndex();
}catch (Exception ex){
//lets clear out any existing info
this.indexFile.createNewFile();
this.dataFile.createNewFile();
this.emptyPages = new TreeSet<Integer>();
this.pageIndex = new HashMap<NodeIdentifier, Entry>();
this.featureTypes = new HashSet<FeatureType>();
}
}
data_file = new RandomAccessFile(f, "rw");
data_channel = data_file.getChannel();
}
/** Factory method : create a new Storage of type DiskStorage.
*
* Valid properties are :
* <ul>
* <li>DiskStorage.DATA_FILE_PROPERTY : filename (mandatory) ; overrides given file if index is not provided.
* <li>DiskStorage.INDEX_FILE_PROPERTY : filename ;
* if exists, must be a valid index file
* and data file must be the valid data file associated with this index.
* <li>DiskStorage.PAGE_SIZE_PROPERTY : int, required if INDEX_FILE does not exist, or is not provided.
* </ul>
* @param property set
* @return new instance of DiskStorage
*/
public static Storage createInstance(Properties pset) {
try {
File f = new File(pset.getProperty(DATA_FILE_PROPERTY));
if (pset.containsKey(INDEX_FILE_PROPERTY)) {
File index = new File(pset.getProperty(INDEX_FILE_PROPERTY));
if (index.exists()) {
return new DiskStorage(f, index);
} else {
int page_size = Integer.parseInt(pset.getProperty(PAGE_SIZE_PROPERTY));
return new DiskStorage(f, page_size, index);
}
} else {
int page_size = Integer.parseInt(pset.getProperty(PAGE_SIZE_PROPERTY));
return new DiskStorage(f, page_size);
}
} catch (IOException e) {
logger.log(Level.WARNING, "DiskStorage : error occured when creating new instance : "+e.getMessage(),e);
return null;
} catch (NullPointerException e) {
throw new IllegalArgumentException("DiskStorage : invalid property set.",e);
}
}
/** Default factory method : create a new Storage of type DiskStorage,
* with page size set to default 1000 bytes, and data file is a new temporary file.
*
* @return new instance of DiskStorage with default parameters.
*/
public static Storage createInstance() {
try {
return new DiskStorage(File.createTempFile("storage", ".tmp"), 1000);
} catch (IOException e) {
logger.log(Level.WARNING,
"DiskStorage : error occured when creating new instance : " + e);
return null;
}
}
/**
* Removes all entries from the disk store and clears the
* associated feature types.
*/
public synchronized void clear() {
for (Iterator<java.util.Map.Entry<NodeIdentifier, Entry>> it = pageIndex.entrySet().iterator();it.hasNext();) {
java.util.Map.Entry<NodeIdentifier, Entry> next = it.next();
Entry e = next.getValue();
int n = 0;
while (n < e.pages.size()) {
emptyPages.add(e.pages.get(n));
n++;
}
it.remove();
}
}
/**
* Gets a particular node
*/
public synchronized Node get(NodeIdentifier id) {
Node node = null;
Entry e = pageIndex.get(id);
if (e == null) {
return null;
}
byte[] data = new byte[e.length];
readData(data, e);
try {
node = readNode(data, id);
} catch (IOException e1) {
throw new IllegalStateException(e1);
} catch (ClassNotFoundException e1) {
throw new IllegalStateException(e1);
} catch (Exception ex){
logger.log(Level.WARNING, "Error reading node.", ex);
}
return node;
}
/*
* Reads data from the file into data array
*/
private void readData(byte[] data, Entry e) {
ByteBuffer buffer = ByteBuffer.allocate(page_size);
int page = 0;
int rem = data.length;
int len = 0;
int next = 0;
int index = 0;
while (next < e.pages.size()) { //for each page
page = e.pages.get(next);
len = (rem > page_size) ? page_size : rem;
try {
buffer.clear();
data_channel.position(page * page_size);
int bytes_read = data_channel.read(buffer);
if (bytes_read != page_size) {
throw new IllegalStateException("Data file might be corrupted.");
}
buffer.rewind();
buffer.get(data, index, len);
rem -= bytes_read;
index += bytes_read;
next++;
} catch (IOException io) {
throw new IllegalStateException(io);
}
}
}
/*
* Converts an array of bytes into a node
*/
private Node readNode(byte[] data, NodeIdentifier id) throws IOException, ClassNotFoundException {
ByteArrayInputStream bais = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bais);
Node node = null;
try{
node = (Node) ois.readObject();
}finally{
ois.close();
bais.close();
}
id = findUniqueInstance(id);
node.setIdentifier(id);
return node;
}
/**
* Adds a node to the store.
*/
public synchronized void put(Node n) {
byte[] data = null;
try {
data = writeNode(n);
} catch (IOException e1) {
logger.log(Level.SEVERE, "Cannot put data in DiskStorage : " + e1);
return;
}
Entry e = new Entry(n.getIdentifier());
Entry old = null;
if (pageIndex.containsKey(e.id)) {
old = pageIndex.get(e.id);
if (old == null) {
// problem
throw new IllegalStateException("old entry null");
}
} else {
if (!pageIndex.containsKey(e.id)) {
pageIndex.put(e.id, null); // advertise we created a new entry } else {
old = pageIndex.get(e.id);
}
}
writeData(data, e, old);
pageIndex.put(e.id, e);
}
/*
* converts a node to a byte array
*/
private byte[] writeNode(Node n) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
try{
oos.writeObject(n);
byte[] data = baos.toByteArray();
stats_bytes += data.length;
stats_n++;
return data;
}finally{
oos.close();
baos.close();
}
}
/*
* writes a btye array to particular pages
*/
private void writeData(byte[] data, Entry e, Entry old) {
ByteBuffer buffer = ByteBuffer.allocate(page_size);
e.length = data.length;
int rem = data.length;
int page;
int len;
int index = 0;
int next = 0;
while (rem > 0) {
if ((old != null) && (next < old.pages.size())) {
page = old.pages.get(next);
next++;
} else if (!emptyPages.isEmpty()) {
synchronized (emptyPages) {
Integer i = emptyPages.first();
page = i.intValue();
if (!emptyPages.remove(i)) {
throw new RuntimeException("buggy here !!!!");
}
}
} else {
page = nextPage++;
}
len = (rem > page_size) ? page_size : rem;
buffer.clear();
buffer.put(data, index, len);
try {
buffer.rewind();
data_channel.position(page * page_size);
data_channel.write(buffer);
} catch (IOException io) {
throw new IllegalStateException(io);
}
rem -= len;
index += len;
e.pages.add(new Integer(page));
}
if (old != null) { // don't forget to recycle pages
while (next < old.pages.size()) {
emptyPages.add(new Integer(old.pages.get(next)));
next++;
}
}
}
/**
* Removes a node from the store.
*/
public synchronized void remove(NodeIdentifier id) {
Entry e = pageIndex.get(id);
if (e == null) {
// problem
throw new IllegalArgumentException("Invalid identifier " + id.toString());
}
int next = 0;
while (next < e.pages.size()) {
emptyPages.add(new Integer(e.pages.get(next)));
next++;
}
pageIndex.remove(id);
}
/**
* Disposes of the store.
* <p>This flushes all data and closes file handles</p>
*/
public synchronized void dispose(){
flush();
try{
this.data_channel.close();
this.data_file.close();
}catch (Exception ex){
logger.log(Level.WARNING, "Error disposing of disk storage", ex);
}
}
/**
* Writes the index file.
* <p>This does not close the data files.</p>
*/
public void flush() {
try {
FileOutputStream os = new FileOutputStream(indexFile);
ObjectOutputStream oos = new ObjectOutputStream(os);
try {
oos.writeInt(this.page_size);
oos.writeInt(this.nextPage);
oos.writeObject(this.emptyPages);
oos.writeObject(this.pageIndex);
oos.writeObject(this.bounds);
oos.writeInt(this.featureTypes.size());
for( Iterator<FeatureType> iterator = featureTypes.iterator(); iterator.hasNext(); ) {
FeatureType type = (FeatureType) iterator.next();
String rep = DataUtilities.spec((SimpleFeatureType) type);
oos.writeObject(type.getName().getNamespaceURI() + "." + type.getName().getLocalPart());
oos.writeObject(rep);
}
} finally {
oos.close();
os.close();
}
} catch (IOException e) {
logger.log(Level.WARNING, "Cannot close DiskStorage normally : " + e, e);
}
}
/**
* Initializes the store from the index file.
*
* @throws IOException
*/
protected void initializeFromIndex() throws IOException {
FileInputStream is = new FileInputStream(indexFile);
ObjectInputStream ois = new ObjectInputStream(is);
try {
this.page_size = ois.readInt();
this.nextPage = ois.readInt();
this.emptyPages = (TreeSet<Integer>) ois.readObject();
this.pageIndex = (HashMap<NodeIdentifier, Entry>) ois.readObject();
this.bounds = (ReferencedEnvelope)ois.readObject();
int featuretypesize = ois.readInt();
this.featureTypes = new HashSet<FeatureType>();
for(int i = 0; i < featuretypesize; i++){
String name = (String)ois.readObject();
String rep = (String)ois.readObject();
try {
FeatureType ft = DataUtilities.createType(name, rep);
featureTypes.add(ft);
} catch (SchemaException e) {
logger.log(Level.WARNING, "Error initializing feature types from store.", e);
}
}
} catch (ClassNotFoundException e) {
throw (IOException) new IOException().initCause(e);
} finally {
ois.close();
is.close();
}
}
public Properties getPropertySet() {
Properties pset = new Properties();
try {
pset.setProperty(STORAGE_TYPE_PROPERTY, DiskStorage.class.getCanonicalName());
pset.setProperty(DATA_FILE_PROPERTY, dataFile.getCanonicalPath());
pset.setProperty(INDEX_FILE_PROPERTY, indexFile.getCanonicalPath());
pset.setProperty(PAGE_SIZE_PROPERTY, new Integer(page_size).toString());
} catch (IOException e) {
logger.log(Level.WARNING, "Error while creating DiskStorage property set : " + e);
}
return pset;
}
public NodeIdentifier findUniqueInstance(NodeIdentifier id) {
if (pageIndex.containsKey(id)) {
return pageIndex.get(id).id;
} else {
return id;
}
}
void logPageAccess(int page, int length) throws IOException {
File log = new File("log/" + page + ".log");
FileWriter fw = new FileWriter(log, true);
try{
fw.write(System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " writing " + length + " bytes.\n");
}finally{
fw.close();
}
}
void logGet() throws IOException {
FileWriter getlog = new FileWriter("log/get.log", true);
try{
getlog.write(Thread.currentThread().getName() + " : " + System.currentTimeMillis() + "\n");
}finally{
getlog.close();
}
}
void writeReadable(Node n, int page) {
try {
FileWriter fw = new FileWriter("log/" + page + ".node");
try{
fw.write(((GridNode) n).toReadableText());
}finally{
fw.close();
}
} catch (IOException e) {
logger.log(Level.WARNING, "Error writing node.", e);
}
}
/**
* Adds a feature type to the store.
*/
public void addFeatureType( FeatureType ft ) {
featureTypes.add(ft);
}
/**
* Gets the feature types supported by the store.
*/
public Collection<FeatureType> getFeatureTypes() {
return Collections.unmodifiableCollection(this.featureTypes);
}
/**
* Clears all feature types associated with store
*/
public void clearFeatureTypes(){
this.featureTypes.clear();
}
/**
* Sets the bounds of the store
*/
public void setBounds(ReferencedEnvelope bounds){
this.bounds = bounds;
}
/**
* Get the bounds of data in the store.
*/
public ReferencedEnvelope getBounds(){
return this.bounds;
}
}
/**
* This is a class to track the pages a particular
* node is written to.
*
*/
class Entry implements Serializable {
private static final long serialVersionUID = -9013786524696213884L;
protected int length = 0;
protected NodeIdentifier id;
protected ArrayList<Integer> pages = new ArrayList<Integer>();
Entry(NodeIdentifier id) {
this.id = id;
}
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append("Id : " + id);
sb.append(", Length : " + length);
for (Iterator<Integer> it = pages.iterator(); it.hasNext();) {
sb.append("\n page = " + it.next());
}
return sb.toString();
}
}