/*
* @(#)$Id: DbCollection.java 3619 2008-03-26 07:23:03Z yui $
*
* Copyright 2006-2008 Makoto YUI
*
* Licensed 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.
*
* Contributors:
* Makoto YUI - initial implementation
*/
package xbird.storage;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Collection;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import xbird.config.Settings;
import xbird.storage.tx.Transaction;
import xbird.util.concurrent.reference.FinalizableSoftValueReferenceMap;
import xbird.util.concurrent.reference.ReferentFinalizer;
import xbird.util.io.FileUtils;
import xbird.util.io.IOUtils;
import xbird.util.resource.PropertyMap;
import xbird.xquery.dm.dtm.DocumentTableLoader;
import xbird.xquery.dm.dtm.IDocumentTable;
import xbird.xquery.dm.dtm.LazyDTMDocument;
import xbird.xquery.dm.instance.DocumentTableModel;
import xbird.xquery.dm.instance.DocumentTableModel.DTMDocument;
import xbird.xquery.meta.DynamicContext;
import xbird.xquery.misc.IStringChunk;
import xbird.xquery.misc.QNameTable;
import xbird.xquery.misc.StringChunkLoader;
/**
*
* <DIV lang="en"></DIV>
* <DIV lang="ja"></DIV>
*
* @author Makoto YUI (yuin405+xbird@gmail.com)
*/
public final class DbCollection implements Closeable {
private static final Log LOG = LogFactory.getLog(DbCollection.class);
public static final String QNAMES_FILE_SUFFIX = ".qnames";
private static final String DTM_PROPS_FILE_SUFFIX = ".dtmp";
private static final String ROOT_COLLECTION_NAME = "/";
public static final String DATA_DIR;
private static final Map<String, DbCollection> _collectionCache;
private static final DbCollection _rootCol;
static {
String dataDir = Settings.get("xbird.database.datadir");
if(dataDir == null) {
String tmp = System.getProperty("java.io.tmpdir");
File file = new File(tmp, "xbird");
if(!file.exists() || file.isFile()) {
file.mkdir();
}
dataDir = file.getAbsolutePath();
LOG.info("Use `" + dataDir + "' for the data repository");
}
DATA_DIR = dataDir;
_collectionCache = new FinalizableSoftValueReferenceMap<String, DbCollection>(new ReferentFinalizer<String, DbCollection>() {
public void finalize(String key, DbCollection reclaimed) {
IOUtils.closeQuietly(reclaimed);
}
});
_rootCol = new DbCollection();
}
private final DbCollection _parent;
private final String _colName;
private final String _absolutePath;
private final Symbols _symbols;
private volatile PropertyMap _properties = null;
private volatile IStringChunk _stringChunk = null;
/** creates root collection */
private DbCollection() {
this._parent = null;
this._colName = ROOT_COLLECTION_NAME;
this._absolutePath = DATA_DIR;
this._symbols = null;
}
private DbCollection(String name, DbCollection parent) throws DbException {
if(name == null || name.indexOf('/') != -1) {
throw new DbException("Collection name must not contain '/', but was '" + name + '\'');
}
this._parent = parent;
this._colName = name;
this._absolutePath = parent.getAbsolutePath() + File.separatorChar + name;
this._symbols = (parent == _rootCol) ? loadSymbols() : parent.getSymbols();
}
public PropertyMap getCollectionProperties() throws IOException {
if(_properties == null) {
synchronized(this) {
if(_properties == null) {
this._properties = generatePropertyMap(_absolutePath, _colName);
}
}
}
return _properties;
}
private static PropertyMap generatePropertyMap(String colDir, String colName)
throws IOException {
String propFilename = colName + DTM_PROPS_FILE_SUFFIX;
File propFile = new File(colDir, propFilename);
if(!propFile.exists()) {
return new PropertyMap(propFile);
}
PropertyMap map = PropertyMap.load(propFile);
return map;
}
public IStringChunk getStringChunk() throws IOException {
synchronized(this) {
if(_stringChunk == null || _stringChunk.getAndIncrementReferenceCount() == 0) {
this._stringChunk = StringChunkLoader.load(this);
}
}
return _stringChunk;
}
public static DbCollection getRootCollection() {
return _rootCol;
}
private Symbols loadSymbols() throws DbException {
return new Symbols(loadQNameTable());
}
private QNameTable loadQNameTable() throws DbException {
File colDir = new File(getAbsolutePath());
if(!colDir.exists()) {
throw new DbException("Collection does not exist: " + colDir.getAbsolutePath());
}
if(!colDir.isDirectory()) {
throw new DbException("Collection '" + colDir.getAbsolutePath()
+ "' is not a directory, but was a file");
}
File symbolFile = new File(colDir, _colName + QNAMES_FILE_SUFFIX);
if(!symbolFile.exists()) {
return new QNameTable(128);
}
// load symbols
final ObjectInputStream ois;
try {
ois = new ObjectInputStream(new FileInputStream(symbolFile));
} catch (FileNotFoundException fe) {
throw new DbException(fe);
} catch (IOException ioe) {
throw new DbException(ioe);
}
final QNameTable symbols;
try {
symbols = (QNameTable) ois.readObject();
} catch (IOException ioe) {
throw new DbException(ioe);
} catch (ClassNotFoundException ce) {
throw new DbException(ce);
} finally {
try {
ois.close();
} catch (IOException e) {
}
}
symbols.setDirty(false);
return symbols;
}
public DbCollection createCollection(String colName) throws DbException {
if(colName == null || colName.indexOf('/') != -1) {
throw new DbException("Collection name must not contain '/', but was " + colName);
}
DbCollection coll = _collectionCache.get(colName);
if(coll != null) {
return coll;
}
File baseDir = getDirectory();
File colDir = new File(baseDir, colName);
if(colDir.exists()) {
return new DbCollection(colName, this);
}
if(!baseDir.canWrite()) {
throw new DbException("Could not write file. Check the permission of "
+ baseDir.getAbsolutePath());
}
if(!colDir.mkdir()) {
throw new IllegalStateException("create directory failed: " + colDir.getAbsolutePath());
}
coll = new DbCollection(colName, this);
_collectionCache.put(colName, coll);
return coll;
}
public boolean removeCollection(String colName) throws DbException {
File baseDir = getDirectory();
File colDir = new File(baseDir, colName);
if(!colDir.exists()) {
return false;
}
boolean deleted = colDir.delete();
return deleted;
}
public void putDocument(String docName, IDocumentTable doc) throws DbException {
Transaction tx = new Transaction();
putDocument(tx, docName, doc);
tx.commit();
}
public void putDocument(Transaction tx, String docName, IDocumentTable doc) throws DbException {
try {
doc.flush(this, docName);
} catch (IOException e) {
throw new DbException("putDocument failed: " + docName, e);
}
}
public boolean removeDocument(Transaction tx, String docName) throws DbException {
assert (docName != null);
// remove from cache
String docPath = getAbsolutePath() + File.separatorChar + docName;
IDocumentTable removedDoc = DocumentTableLoader.removeDocument(docPath);
if(removedDoc != null) {
try {
removedDoc.close();
} catch (IOException e) {
LOG.warn("Failed to close a document: " + docPath, e);
}
}
// deletes indices and document itself.
final List<File> files = FileUtils.listFiles(getDirectory(), new String[] { docName }, null, true);
if(files.isEmpty()) {
return false;
}
for(File file : files) {
if(!file.delete()) {
return false;
}
}
return true;
}
public Map<String, DTMDocument> listDocuments(DynamicContext dynEnv) throws DbException {
return listDocuments(null, true, dynEnv);
}
public Map<String, DTMDocument> listDocuments(String filterExp, boolean lazy, DynamicContext dynEnv)
throws DbException {
final Collection<File> files = FileUtils.listFiles(getDirectory(), new String[] { IDocumentTable.DTM_SEGMENT_FILE_SUFFIX }, false);
final Map<String, DTMDocument> colls = new IdentityHashMap<String, DTMDocument>(files.size());
for(File f : files) {
String fname = FileUtils.getFileName(f);
String dname = fname.substring(0, fname.lastIndexOf('.'));
if(filterExp != null && !dname.matches(filterExp)) {
continue;
}
final DTMDocument doc;
if(lazy) {
doc = new LazyDTMDocument(dname, this, dynEnv);
} else {
doc = getDocument(null, dname, dynEnv);
}
colls.put(dname, doc);
}
return colls;
}
public List<File> listDocumentFiles(boolean recursive) {
return FileUtils.listFiles(getDirectory(), new String[] { IDocumentTable.DTM_SEGMENT_FILE_SUFFIX }, recursive);
}
public boolean containsDocument(String docName) {
return !FileUtils.listFiles(getDirectory(), new String[] { docName }, new String[] { IDocumentTable.DTM_SEGMENT_FILE_SUFFIX }, false).isEmpty();
}
public DTMDocument getDocument(Transaction tx, String docName, DynamicContext dynEnv)
throws DbException {
final IDocumentTable dtm;
try {
dtm = DocumentTableLoader.load(this, docName, dynEnv);
} catch (IOException e) {
throw new DbException("loading document failed: " + docName, e);
}
return new DocumentTableModel(dtm, true).documentNode();
}
public void flushSymbols() throws DbException {
_symbols.flush(this);
}
public DbCollection getParentCollection() {
return _parent;
}
public String getCollectionName() {
return _colName;
}
public String getAbsolutePath() {
return _absolutePath;
}
@Deprecated
public String getRelativePath() {
final int rootLength = _rootCol.getAbsolutePath().length();
String rawRelativePath = _absolutePath.substring(rootLength);
return rawRelativePath.replace(File.separatorChar, '/');
}
public Symbols getSymbols() {
return _symbols;
}
public File getDirectory() {
String baseDir = getAbsolutePath();
File colDir = new File(baseDir);
return colDir;
}
public static class Symbols {
private boolean dirty = true;
private final QNameTable qnameTable;
public Symbols(QNameTable qnameTable) {
if(qnameTable == null) {
throw new IllegalArgumentException();
}
this.qnameTable = qnameTable;
}
public QNameTable getQnameTable() {
return qnameTable;
}
public boolean isDirty() {
return dirty;
}
public void setDirty(boolean dirty) {
this.dirty = dirty;
}
public synchronized void flush(DbCollection col) throws DbException {
if(!dirty) {
return;
}
qnameTable.flush(col);
}
}
public static DbCollection getCollection(String colpath) {
if(colpath == null) {
throw new IllegalArgumentException();
}
if(!colpath.startsWith("/")) {
throw new IllegalArgumentException("Illegal collection path: " + colpath);
}
DbCollection coll = _collectionCache.get(colpath);
if(coll != null) {
return coll;
}
String[] colnames = colpath.split("/");
int lastidx = colnames.length - 1;
for(int i = 0; i < colnames.length; i++) {
String colname = colnames[i];
if(colname.contains(".")) {
lastidx = i - 1;
}
}
try {
coll = DbCollection.getRootCollection();
for(int i = 1; i <= lastidx; i++) {
String colname = colnames[i];
coll = new DbCollection(colname, coll);
}
_collectionCache.put(colpath, coll);
return coll;
} catch (DbException e) {
return null;
}
}
public static String getDocumentFilterExp(String colpath) {
String[] colnames = colpath.split("/");
if(colnames.length == 0) {
return null;
}
String lastName = colnames[colnames.length - 1];
return lastName.contains(".") ? lastName : null;
}
@Override
public String toString() {
return _colName + " [" + _absolutePath + ']';
}
public void close() throws IOException {
this._properties = null;
IStringChunk sc = _stringChunk;
if(sc != null) {
sc.close();
this._stringChunk = null;
}
}
}