package com.fourspaces.featherdb.backend;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import com.fourspaces.featherdb.FeatherDB;
import com.fourspaces.featherdb.document.Document;
import com.fourspaces.featherdb.document.DocumentCreationException;
import com.fourspaces.featherdb.utils.FileUtils;
import com.fourspaces.featherdb.utils.LineCallback;
import com.fourspaces.featherdb.utils.Lock;
import com.fourspaces.featherdb.utils.Logger;
/**
* Stores documents as files / directories. All of the information is stored
*
* The file structure is: / db / doc id / _revisions - list of revisions in
* order - one line per revision, most recent is last / _common - common data
* for all revisions (created date, current revision, etc... ) in JSON format /
* _permissions - the permissions for this document / rev_id - a file for each
* revision in JSON format
*
* @author mbreese
*
*/
public class FileSystemBackend implements Backend {
private FeatherDB featherDB;
private File rootDir;
final private Logger log = Logger.get(FileSystemBackend.class);
public FileSystemBackend() {
}
private File dbDir(String db) {
return new File(rootDir, db);
}
private File docDir(String db, String id) {
// if (id.indexOf("/")>-1) {
// id.replaceAll("/", "\\/");
// }
return new File(dbDir(db), id);
}
public void deleteDocument(String db, String id) throws BackendException {
File docDir = docDir(db, id);
if (!docDir.exists()) {
log.warn("Deleting non-existant document {}/{}", db,id);
return;
}
log.debug("Deleting document {}/{}", db, id);
deleteRecursive(docDir);
}
public void init() {
}
public void shutdown() {
}
public Iterable<Document> allDocuments(final String db) {
return getDocuments(db, null);
}
protected void findAllDocuments(List<String> ids, File baseDir, String baseName) {
for (File f: baseDir.listFiles()) {
if (f.isDirectory()) {
if (new File(f,"_common").exists()) {
log.debug("found _common file in {}",f.getAbsolutePath());
if (baseName!=null) {
log.debug("adding id: {}",baseName+"/"+f.getName());
ids.add(baseName+"/"+f.getName());
} else {
log.debug("adding id: {}",f.getName());
ids.add(f.getName());
}
} else {
if (baseName!=null) {
findAllDocuments(ids,f,baseName+"/"+f.getName());
} else {
findAllDocuments(ids,f,f.getName());
}
}
}
}
}
public Iterable<Document> getDocuments(final String db, final String[] ids) {
File dbDir = dbDir(db);
final String[] idList;
if (ids == null) {
List<String> existingIds = new ArrayList<String>();
findAllDocuments(existingIds,dbDir,null);
idList = new String[existingIds.size()];
int i=0;
for (String id:existingIds) {
log.debug("found doc id => {}",id);
idList[i++] = id;
}
} else {
idList = ids;
}
final Iterator<Document> i = new Iterator<Document>() {
int index = 0;
Document nextDoc = null;
public void findNext() {
nextDoc = null;
while (idList != null && index < idList.length && nextDoc == null) {
nextDoc = getDocument(db,idList[index]);
index++;
}
}
public boolean hasNext() {
if (index == 0 && nextDoc == null) {
findNext();
}
return nextDoc != null;
}
public Document next() {
Document nd = nextDoc;
findNext();
return nd;
}
public void remove() {
findNext();
}
};
return new Iterable<Document>() {
public Iterator<Document> iterator() {
return i;
}
};
}
public Set<String> getDatabaseNames() {
Set<String> dbs = new HashSet<String>();
for (File f : rootDir.listFiles()) {
if (f.isDirectory()) {
dbs.add(f.getName());
}
}
return dbs;
}
public Document getDocument(String db, String id) {
return getDocument(db, id, null);
}
public Document getDocument(String db, String id, String rev) {
File docDir = docDir(db, id);
log.debug("Retrieving document {}/{}/{}", db, id, rev);
try {
File commonFile = new File(docDir, "_common");
if (!commonFile.exists()) {
log.warn("Document _common file not found: {}/{}",db,id);
return null;
}
JSONObject commonJSON = JSONObject.read(new FileInputStream(commonFile));
if (rev == null) {
rev = commonJSON.getString("_current_revision");
}
File metaFile = new File(docDir, rev+".meta");
if (!metaFile.exists()) {
log.warn("Document .meta file not found: {}/{}/{}",db,id,rev);
return null;
}
JSONObject metaJSON = JSONObject.read(new FileInputStream(metaFile));
Document d = Document.loadDocument(commonJSON,metaJSON);
if (d.writesRevisionData()) {
File revFile = new File(docDir, rev);
if (!revFile.exists()) {
log.warn("Document revision file not found: {}/{}/{}",db,id,rev);
return null;
}
d.setRevisionData(new FileInputStream(revFile));
}
return d;
} catch (JSONException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (DocumentCreationException e) {
e.printStackTrace();
}
return null;
}
public void addDatabase(String name) throws BackendException {
File dir = new File(rootDir, name);
if (dir.exists()) {
log.error("Database dir '{}' already exists!",name);
throw new BackendException("The database " + name
+ " already exists!");
}
log.debug("Adding database: {}",name);
dir.mkdir();
}
public void deleteDatabase(String name) throws BackendException {
File dir = dbDir(name);
if (dir.exists() && dir.isDirectory()) {
deleteRecursive(dir);
}
}
private void deleteRecursive(File dir) {
if (dir.isDirectory()) {
for (File child : dir.listFiles()) {
if (child.isDirectory()) {
deleteRecursive(child);
}
child.delete();
}
}
if (dir.exists()) {
dir.delete();
}
}
public boolean doesDatabaseExist(String db) {
return dbDir(db).exists();
}
public boolean doesDocumentExist(String db, String id) {
return docDir(db, id).exists();
}
public boolean doesDocumentRevisionExist(String db, String id,
String revision) {
return new File(docDir(db, id), revision).exists();
}
public Document saveDocument(Document doc) throws BackendException {
JSONObject commonJSON = doc.getCommonData();
commonJSON.put("_current_revision", doc.getRevision());
File docDir = docDir(doc.getDatabase(), doc.getId());
if (!docDir.exists()) {
log.info("Creating document dir: {}",docDir(doc.getDatabase(),doc.getId()));
docDir.mkdirs();
try {
File commonFile = new File(docDir, "_common"); // for common
// data elements
commonFile.createNewFile();
FileUtils.writeToFile(commonFile, "{}");
new File(docDir, "_revisions").createNewFile(); // for keeping
// revisions in
// order
} catch (IOException e) {
throw new BackendException(e);
}
}
Lock lock = Lock.lock(docDir);
log.info("updating revision file : {} #{}",doc.getId(),doc.getRevision());
// write out all revision'd elements
if (doc.isDataDirty()) {
try {
File revisionMetaFile = new File(docDir, doc.getRevision()+".meta");
// add the current revision to the _revisions file (if needed)
if (!revisionMetaFile.exists()) {
File revListFile = new File(docDir, "_revisions");
FileUtils.writeToFile(revListFile, doc.getRevision() + "\n", true);
// write the document's revision data if req'd
if (doc.writesRevisionData()) {
File revisionFile = new File(docDir, doc.getRevision());
OutputStream out = new FileOutputStream(revisionFile);
doc.writeRevisionData(out);
out.close();
}
// write the meta json data
Writer metaWriter = new FileWriter(revisionMetaFile);
doc.getMetaData().write(metaWriter);
metaWriter.close();
}
} catch (IOException e) {
throw new BackendException(e);
}
}
log.info("updating common file : {}",doc.getId());
// write out all common elements ( if the data is dirty, there is a new current_rev, so a new common
// needs to be written
if (doc.isCommonDirty() || doc.isDataDirty()) {
try {
File commonFile = new File(docDir, "_common");
System.err.println("Writing common: "+commonJSON.toString(2));
Writer writer = new FileWriter(commonFile);
commonJSON.write(writer);
writer.close();
} catch (IOException e) {
throw new BackendException(e);
}
}
lock.release();
featherDB.recalculateViewForDocument(doc);
return doc;
}
public JSONArray getDocumentRevisions(String db, final String id) {
final JSONArray ar = new JSONArray();
File docDir = docDir(db, id);
File revListFile = new File(docDir, "_revisions");
try {
FileUtils.readFileByLine(revListFile,new LineCallback() {
public void process(String line) {
ar.put(line.trim());
}
});
} catch (IOException e) {
// I don't like silent exceptions...
log.error("Error loading revisions for {}/{}",db,id);
}
return ar;
}
public Map<String, Object> getDatabaseStats(String name) {
Map<String, Object> m = new HashMap<String, Object>();
m.put("db_name", name);
int count = 0;
for (File f : dbDir(name).listFiles()) {
if (f.isDirectory()) {
count++;
}
}
m.put("doc_count", count);
return m;
}
public void init(FeatherDB featherDB) {
this.featherDB=featherDB;
String path = featherDB.getProperty("backend.fs.path");
if (path == null) {
throw new RuntimeException(
"You must include a backend.fs.path element in coffeedb.properties or specify the path in the constructor");
}
log.info("Using database path {}", path);
this.rootDir = new File(path);
if (!rootDir.exists()) {
log.debug("Creating database directory");
rootDir.mkdirs();
} else if (!rootDir.isDirectory()) {
log.error("Path: {} not valid!", path);
throw new RuntimeException("Path: " + path + " not valid!");
}
}
/**
* This makes a place-holder file to avoid revision name duplicates.
*/
public void touchRevision(String db, String id, String rev) {
try {
docDir(db, id).mkdirs();
new File(docDir(db, id), rev).createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
}