/* * Copyright 2011 Future Systems, Inc. * * 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. */ package org.krakenapps.confdb.file; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import org.krakenapps.codec.EncodingRule; import org.krakenapps.confdb.CollectionEntry; import org.krakenapps.confdb.CommitOp; import org.krakenapps.confdb.Config; import org.krakenapps.confdb.ConfigCollection; import org.krakenapps.confdb.ConfigEntry; import org.krakenapps.confdb.ConfigIterator; import org.krakenapps.confdb.ConfigTransaction; import org.krakenapps.confdb.Manifest; import org.krakenapps.confdb.Predicate; import org.krakenapps.confdb.RollbackException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class FileConfigCollection implements ConfigCollection { private final Logger logger = LoggerFactory.getLogger(FileConfigCollection.class.getName()); private FileConfigDatabase db; /** * head revision if null, otherwise it point to specific revision */ private Integer changeset; /** * TODO: manifest should be updated when you commit */ /** * collection metadata */ private CollectionEntry col; private File logFile; private File datFile; public FileConfigCollection(FileConfigDatabase db, Integer changeset, CollectionEntry col) throws IOException { File dbDir = db.getDbDirectory(); boolean created = dbDir.mkdirs(); if (created) logger.info("kraken confdb: created database dir [{}]", dbDir.getAbsolutePath()); this.db = db; this.col = col; this.changeset = changeset; logFile = new File(dbDir, "col" + col.getId() + ".log"); datFile = new File(dbDir, "col" + col.getId() + ".dat"); } public int getId() { return col.getId(); } @Override public String getName() { return col.getName(); } @Override public int count() { Manifest manifest = db.getManifest(changeset); return manifest.getConfigEntries(col.getName()).size(); } @Override public int count(Predicate pred) { try { ConfigIterator it = getIterator(pred); return it.count(); } catch (IOException e) { throw new IllegalStateException("cannot count " + col.getName() + " of database " + db.getName(), e); } } @Override public int count(ConfigTransaction xact) { Manifest manifest = xact.getManifest(); return manifest.getConfigEntries(col.getName()).size(); } @Override public ConfigIterator findAll() { return find(null); } @Override public Config findOne(Predicate pred) { ConfigIterator it = null; Config c = null; try { it = find(pred); if (it.hasNext()) c = it.next(); return c; } finally { if (it != null) it.close(); } } @Override public ConfigIterator find(Predicate pred) { try { return getIterator(pred); } catch (FileNotFoundException e) { // collection has no data logger.debug("kraken confdb: returns empty iterator, db [{}] col [{}]", db.getName(), col.getName()); return new EmptyIterator(); } catch (IOException e) { throw new IllegalStateException("cannot open collection file", e); } } private ConfigIterator getIterator(Predicate pred) throws IOException { Manifest manifest = db.getManifest(changeset); RevLogReader reader = new RevLogReader(logFile, datFile); List<RevLog> snapshot = getSnapshot(manifest, reader); if (logger.isDebugEnabled()) logger.debug("kraken confdb: db [{}], col [{}], snapshot size [{}]", new Object[] { db.getName(), col.getName(), snapshot.size() }); return new FileConfigIterator(db, manifest, this, reader, snapshot, pred); } private List<RevLog> getSnapshot(Manifest manifest, RevLogReader reader) throws IOException { List<RevLog> snapshot = db.getSnapshotCache(col.getId(), manifest.getId()); if (snapshot != null) { if (logger.isDebugEnabled()) logger.debug("kraken confdb: return cached snapshot, db [{}], col [{}], manifest [{}], snapshot size [{}]", new Object[] { db.getName(), col.getName(), manifest.getId(), snapshot.size() }); return snapshot; } snapshot = new ArrayList<RevLog>(); if (manifest.getVersion() == 1) { long count = reader.count(); for (long index = 0; index < count; index++) { RevLog log = reader.read(index); // assume that rev is unique if ((log.getOperation() == CommitOp.CreateDoc || log.getOperation() == CommitOp.UpdateDoc) && manifest.containsDoc(col.getName(), log.getDocId(), log.getRev())) snapshot.add(log); } } else if (manifest.getVersion() == 2) { List<ConfigEntry> configs = manifest.getConfigEntries(col.getName()); for (ConfigEntry c : configs) { RevLog log = reader.read(c.getIndex()); snapshot.add(log); if (logger.isDebugEnabled()) logger.debug("kraken confdb: db [{}], col [{}], manifest [{}], revlog [{}]", new Object[] { db.getName(), col.getName(), manifest.getId(), log }); } db.setSnapshotCache(col.getId(), manifest.getId(), snapshot); } return snapshot; } @Override public Config add(Object doc) { return add(doc, null, null); } @Override public Config add(Object doc, String committer, String log) { ConfigTransaction xact = db.beginTransaction(); try { Config c = add(xact, doc); xact.commit(committer, log); return c; } catch (Throwable e) { xact.rollback(); throw new RollbackException(e); } } @Override public Config add(ConfigTransaction xact, Object doc) { try { RevLogWriter writer = getWriter(xact); ByteBuffer bb = encodeDocument(doc); RevLog revlog = newLog(0, 0, CommitOp.CreateDoc, bb.array()); // write collection log int docId = writer.write(revlog); int index = writer.count() - 1; // write db changelog xact.log(CommitOp.CreateDoc, col.getName(), docId, revlog.getRev(), index); return new FileConfig(db, this, docId, revlog.getRev(), revlog.getPrevRev(), doc); } catch (IOException e) { throw new IllegalStateException("cannot add object", e); } } private ByteBuffer encodeDocument(Object doc) { int len = EncodingRule.lengthOf(doc); ByteBuffer bb = ByteBuffer.allocate(len); EncodingRule.encode(bb, doc); return bb; } @Override public Config update(Config c) { return update(c, false); } @Override public Config update(Config c, boolean checkConflict) { return update(c, checkConflict, null, null); } @Override public Config update(Config c, boolean checkConflict, String committer, String log) { ConfigTransaction xact = db.beginTransaction(); try { Config updated = update(xact, c, checkConflict); xact.commit(committer, log); return updated; } catch (Throwable e) { xact.rollback(); throw new RollbackException(e); } } @Override public Config update(ConfigTransaction xact, Config c, boolean checkConflict) { RevLogReader reader = null; try { Manifest manifest = db.getManifest(changeset); RevLogWriter writer = getWriter(xact); reader = new RevLogReader(logFile, datFile); List<RevLog> snapshot = getSnapshot(manifest, reader); // find any conflict (if common parent exists) long lastRev = c.getRevision(); for (RevLog log : snapshot) { if (log.getDocId() == c.getId() && log.getPrevRev() == lastRev) { if (checkConflict) throw new IllegalStateException("conflict with " + log.getRev()); lastRev = log.getRev(); } } ByteBuffer bb = encodeDocument(c.getDocument()); RevLog revlog = newLog(c.getId(), lastRev, CommitOp.UpdateDoc, bb.array()); // write collection log int id = writer.write(revlog); int index = writer.count() - 1; xact.log(CommitOp.UpdateDoc, col.getName(), id, revlog.getRev(), index); if (logger.isDebugEnabled()) logger.debug("kraken confdb: updated db [{}] col [{}] doc [{}]", new Object[] { db.getName(), col.getName(), c.getDocument() }); return new FileConfig(db, this, id, revlog.getRev(), revlog.getPrevRev(), c.getDocument()); } catch (IOException e) { throw new IllegalStateException("cannot update object", e); } finally { if (reader != null) reader.close(); } } @Override public Config remove(Config c) { return remove(c, false); } @Override public Config remove(Config c, boolean checkConflict) { return remove(c, checkConflict, null, null); } @Override public Config remove(Config c, boolean checkConflict, String committer, String log) { ConfigTransaction xact = db.beginTransaction(); Config config = c; try { config = remove(xact, c, checkConflict); xact.commit(committer, log); return config; } catch (Throwable e) { xact.rollback(); throw new RollbackException(e); } } @Override public Config remove(ConfigTransaction xact, Config c, boolean checkConflict) { try { RevLogWriter writer = getWriter(xact); // TODO: check conflict RevLog revlog = newLog(c.getId(), c.getRevision(), CommitOp.DeleteDoc, null); // write collection log int id = writer.write(revlog); int index = writer.count() - 1; xact.log(CommitOp.DeleteDoc, col.getName(), id, revlog.getRev(), index); return new FileConfig(db, this, id, revlog.getRev(), revlog.getPrevRev(), null); } catch (IOException e) { throw new IllegalStateException("cannot remove object", e); } } private RevLog newLog(int id, long prev, CommitOp op, byte[] doc) { RevLog log = new RevLog(); log.setDocId(id); log.setRev(prev + 1); log.setPrevRev(prev); log.setOperation(op); log.setDoc(doc); return log; } private RevLogWriter getWriter(ConfigTransaction xact) throws IOException { FileConfigTransaction fxact = (FileConfigTransaction) xact; RevLogWriter writer = fxact.getWriters().get(logFile); if (writer == null) { writer = new RevLogWriter(logFile, datFile); fxact.getWriters().put(logFile, writer); } return writer; } @Override public String toString() { return "[" + col.getId() + "] " + col.getName(); } }