rapture.repo.VersionedRepo.java Source code

Java tutorial

Introduction

Here is the source code for rapture.repo.VersionedRepo.java

Source

/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2011-2016 Incapture Technologies LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package rapture.repo;

import rapture.common.LockHandle;
import rapture.common.RaptureConstants;
import rapture.common.RaptureDNCursor;
import rapture.common.RaptureFolderInfo;
import rapture.common.RaptureNativeQueryResult;
import rapture.common.RaptureQueryResult;
import rapture.common.RaptureURI;
import rapture.common.exception.RaptNotSupportedException;
import rapture.common.exception.RaptureException;
import rapture.common.exception.RaptureExceptionFactory;
import rapture.common.impl.jackson.JsonContent;
import rapture.common.model.DocumentAttribute;
import rapture.common.model.DocumentVersionInfo;
import rapture.common.model.DocumentWithMeta;
import rapture.common.model.RaptureCommit;
import rapture.common.model.RemoteLink;
import rapture.common.repo.BaseObject;
import rapture.common.repo.CommentaryObject;
import rapture.common.repo.CommitObject;
import rapture.common.repo.DocumentBagObject;
import rapture.common.repo.DocumentBagReference;
import rapture.common.repo.DocumentObject;
import rapture.common.repo.PerspectiveObject;
import rapture.common.repo.TagObject;
import rapture.common.repo.TreeObject;
import rapture.dsl.dparse.BaseDirective;
import rapture.index.IndexHandler;
import rapture.lock.ILockingHandler;
import rapture.repo.db.KeyedDatabase;
import rapture.repo.db.ObjectDatabase;
import rapture.repo.stage.CommitCollector;
import rapture.repo.stage.Stage;
import rapture.repo.stage.StageTree;
import rapture.util.IDGenerator;

import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

import com.google.common.base.Optional;

/**
 * A repo contains an objectdatabase and references to perspectives It is used
 * to manage access to typed content in Rapture
 *
 * @author amkimian
 */
public class VersionedRepo extends BaseRepo implements Repository {
    private static Logger log = Logger.getLogger(VersionedRepo.class);
    private static final String ERROR_CANNOT_RUN = "Cannot run on versioned repo";

    private static final String OFFICIAL = RaptureConstants.OFFICIAL_STAGE;

    private static final String DEFAULT_STAGE = RaptureConstants.OFFICIAL_STAGE;

    private ObjectDatabase objDb;

    private KeyedDatabase keyDb;
    private KeyStore cacheKeyStore;

    private static String setupKey = "$SETUP";
    private Map<String, Stage> stages;
    private int capacity = 500;
    private ILockingHandler lockHandler;

    public VersionedRepo(Map<String, String> config, KeyStore store, KeyStore cacheKeyStore,
            ILockingHandler lockHandler) {
        super(store, config);
        this.cacheKeyStore = cacheKeyStore;
        this.lockHandler = lockHandler;
        if (config != null && config.containsKey("capacity")) {
            try {
                capacity = Integer.parseInt(config.get("capacity"));
            } catch (Exception e) {
                log.error("Invalid capacity " + config.get("capacity"));
                log.error("Will use default of 500");
            }
        }
        store.resetFolderHandling();
        objDb = new ObjectDatabase(store);
        keyDb = new KeyedDatabase(store);
        stages = new HashMap<String, Stage>();
        PerspectiveObject po = keyDb.getPerspective(OFFICIAL);
        if (po == null) {
            initBlankEnv();
        } else {
            if (!cacheKeyStore.containsKey(setupKey)) {
                log.info("Initializing vcache");
                initCacheStore();
                log.info("Initializing vcache - done");
            }
        }
    }

    @Override
    public void addCommentary(String key, String who, String description, String commentaryKey, String ref) {
        PerspectiveObject po = keyDb.getPerspective(OFFICIAL);
        CommitObject cObj = objDb.getCommit(po.getLatestCommit());
        final CommentaryObject comment = new CommentaryObject();
        comment.setCommentaryKey(commentaryKey);
        comment.setWho(who);
        comment.setWhen(new Date());
        comment.setMessage(description);

        handleCommentaryFromCommit(cObj, key, new CommentaryHandler() {

            @Override
            public boolean handleObject(BaseObject b) {
                try {
                    String ref = objDb.writeCommentary(comment);
                    b.getCommentaryReferences().add(ref);
                    return true;
                } catch (Exception e) {
                    log.error("Could not handle commentary from commit - " + e.getMessage());
                }
                return false;
            }

        });
    }

    @Override
    public DocumentWithMeta addDocument(String key, String value, String user, String comment, boolean mustBeNew) {
        String context = IDGenerator.getUUID();
        LockHandle lockHandle = lockHandler.acquireLock(context, DEFAULT_STAGE, 5, 5);
        if (lockHandle != null) {
            try {
                // Create a new stage to be consistent
                createStage(DEFAULT_STAGE);
                addToStage(DEFAULT_STAGE, key, value, mustBeNew);
                commitStage(DEFAULT_STAGE, user, comment);

                cacheKeyStore.put(key, value);
            } finally {
                lockHandler.releaseLock(context, DEFAULT_STAGE, lockHandle);
            }
        } else {
            throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_INTERNAL_ERROR,
                    "Could not get lock for write");
        }
        return null; // Deprecated repository so don't do anything special here.
    }

    @Override
    public void addDocuments(List<String> keys, String value, String user, String comment) {
        // We want to add these displayNames, but point them to the same
        // document content
        String context = IDGenerator.getUUID();
        LockHandle lockHandle = lockHandler.acquireLock(context, DEFAULT_STAGE, 5, 5);
        if (lockHandle != null) {
            try {
                // Create a new stage to be consistent
                createStage(DEFAULT_STAGE);
                addToStage(DEFAULT_STAGE, keys, value, false);
                commitStage(DEFAULT_STAGE, user, comment);
                for (String x : keys) {
                    cacheKeyStore.put(x, value);
                }
            } finally {
                lockHandler.releaseLock(context, DEFAULT_STAGE, lockHandle);
            }
        } else {
            throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_INTERNAL_ERROR,
                    "Could not get lock for write");
        }
    }

    private void addToStage(String stage, List<String> keys, String document, boolean mustBeNew) {
        Stage st = getStage(stage);
        DocumentObject doc = new DocumentObject();
        doc.setContent(new JsonContent(document));
        for (String path : keys) {
            String[] p = path.split("/");
            LinkedList<String> parts = new LinkedList<String>();
            for (String a : p) {
                parts.add(a);
            }
            st.getStageBase().addDocumentToStage(this, parts, doc, mustBeNew);
        }
    }

    /**
     * Add a document to the stage
     *
     * @throws Exception
     */

    public void addToStage(String stage, String path, String document, boolean mustBeNew) {
        Stage st = getStage(stage);
        DocumentObject doc = new DocumentObject();
        doc.setContent(new JsonContent(document));
        String[] p = path.split("/");
        LinkedList<String> parts = new LinkedList<String>();
        for (String a : p) {
            parts.add(a);
        }
        st.getStageBase().addDocumentToStage(this, parts, doc, mustBeNew);
    }

    @Override
    public void clearRemote() {
        PerspectiveObject po = keyDb.getPerspective(OFFICIAL);
        po.setRemoteLink(null);
        keyDb.writePerspective(OFFICIAL, po);

    }

    public void commitStage(String stage, String user, String comment) {
        Stage st = getStage(stage);
        CommitObject cObj = new CommitObject();
        cObj.setUser(user);
        cObj.setWhen(new Date());
        cObj.setComment(comment);
        cObj.setPreviousReference(st.getPerspective().getLatestCommit());
        String cRef = objDb.getCommitReference(cObj);
        cObj.setCommitRef(cRef);
        CommitCollector collector = new CommitCollector();
        Map<DocumentObject, String> docCollector = new HashMap<DocumentObject, String>();
        String reference = st.getStageBase().commitStage(this, cRef, collector, docCollector);
        collector.addTreeReference(reference);
        cObj.setTreeRef(reference);
        cObj.setChanges(collector.toString());
        cObj.setTreeReferences(collector.getTreeReferences());
        cObj.setDocReferences(collector.getDocReferences());
        objDb.writeCommit(cObj, cRef);
        st.getPerspective().setLatestCommit(cRef);
        // Need to write this back... (perhaps checking that the perspective on
        // db hasn't changed...!)
        keyDb.writePerspective(st.getPerspectiveName(), st.getPerspective());
    }

    @Override
    public long countDocuments() throws RaptNotSupportedException {
        throw new RaptNotSupportedException("Count not supported for versioned repo");
    }

    /**
     * Create a new perspective based on the one passed in
     *
     * @param basePerspective
     * @param name
     * @throws Exception
     */
    public void createPerspective(String basePerspective, String name) {
        PerspectiveObject original = keyDb.getPerspective(basePerspective);
        PerspectiveObject newP = new PerspectiveObject();
        newP.setBaseCommit(original.getLatestCommit());
        newP.setLatestCommit(original.getLatestCommit());
        newP.setOwner("caller");
        keyDb.writePerspective(name, newP);
    }

    /**
     * Create a stage on which to do some changes. A stage is kind of like a
     * realised tree with additional "overrides" on some names which reflect new
     * concepts created
     *
     * @throws Exception
     */

    @Override
    public Stage createStage(String stageName) {
        Stage stage = new Stage();
        PerspectiveObject po = keyDb.getPerspective(OFFICIAL);
        stage.setPerspectiveName(OFFICIAL);
        stage.setPerspective(po);
        CommitObject co = objDb.getCommit(po.getLatestCommit());
        TreeObject to = objDb.getTree(co.getTreeRef());
        StageTree st = new StageTree(to, capacity);
        stage.setStageBase(st);
        stages.put(stageName, stage);
        return stage;
    }

    @Override
    public void createTag(String user, String tagName) {
        PerspectiveObject po = keyDb.getPerspective(OFFICIAL);
        TagObject to = new TagObject();
        to.setCommitRef(po.getLatestCommit());
        to.setOwner(user);
        to.setWhen(new Date());
        keyDb.writeTag(tagName, to);
    }

    @Override
    public void drop() {
        if (cacheKeyStore != null) {
            cacheKeyStore.dropKeyStore();
        }
        if (!store.dropKeyStore()) {
            throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_INTERNAL_ERROR, "Could not drop keystore");
        }
    }

    @Override
    public List<CommentaryObject> getCommentary(String key) {
        // Given a perspective and key, load the associated BasedObject and get
        // the list of references. Then load
        // each CommentaryObject from those references and return that
        PerspectiveObject po = keyDb.getPerspective(OFFICIAL);
        CommitObject cObj = objDb.getCommit(po.getLatestCommit());
        final List<CommentaryObject> ret = new ArrayList<CommentaryObject>();
        handleCommentaryFromCommit(cObj, key, new CommentaryHandler() {

            @Override
            public boolean handleObject(BaseObject b) {
                List<String> commentaryRefs = b.getCommentaryReferences();
                for (String c : commentaryRefs) {
                    CommentaryObject comment;
                    try {
                        comment = objDb.getCommentary(c);
                        if (comment != null) {
                            ret.add(comment);
                        }
                    } catch (RaptureException e) {
                        log.error("Could not load commentary object " + c);
                    }
                }
                return false; // Don't need to save BaseObject back
            }

        });
        return ret;
    }

    @Override
    public List<RaptureCommit> getCommitHistory() {
        return getCommitsSince(null);
    }

    @Override
    public CommitObject getCommitObject(String reference) {
        return objDb.getCommit(reference);
    }

    @Override
    public List<RaptureCommit> getCommitsSince(String commitReference) {
        List<RaptureCommit> commits = new ArrayList<RaptureCommit>();
        PerspectiveObject po = keyDb.getPerspective(OFFICIAL);
        CommitObject cObj = objDb.getCommit(po.getLatestCommit());
        boolean done = false;
        while (!done) {
            RaptureCommit co = new RaptureCommit();
            co.setComment(cObj.getComment());
            co.setWhen(cObj.getWhen());
            co.setWho(cObj.getUser());
            co.setChanges(cObj.getChanges());
            co.setReference(cObj.getCommitRef());
            co.setDocReferences(cObj.getDocReferences());
            co.setTreeReferences(cObj.getTreeReferences());
            commits.add(co);
            String ref = cObj.getPreviousReference();
            if (ref != null && ref.equals(commitReference)) {
                done = true;
            } else {
                if (ref != null) {
                    cObj = objDb.getCommit(ref);
                } else {
                    cObj = null;
                }
                if (cObj == null) {
                    done = true;
                }
            }
        }

        return commits;
    }

    private CommitObject getCommitViaDirective(CommitObject startPoint, String context, BaseDirective directive) {
        if (directive == null) {
            return startPoint;
        }
        directive.reset(context);
        CommitObject cObj = startPoint;
        while (directive.incorrect(cObj)) {
            cObj = objDb.getCommit(cObj.getPreviousReference());
            if (cObj == null) {
                break;
            }
        }
        return directive.retrieveCommit();
    }

    public String getDocument(String documentName) {
        // Retrieve a document from perspective, given its path
        String doc = cacheKeyStore.get(documentName);
        if (doc != null) {
            return doc;
        }

        PerspectiveObject po = keyDb.getPerspective(OFFICIAL);
        CommitObject cObj = objDb.getCommit(po.getLatestCommit());
        return getDocumentFromCommit(documentName, cObj);
    }

    @Override
    public String getDocument(String documentName, BaseDirective directive) {
        if (directive == null) {
            return getDocument(documentName);
        }
        // In this request we start with the given commit object (the latest in
        // the perspective) and
        // use the directive to work back until we find the correct commit. Then
        // we get the document
        // from that commit
        PerspectiveObject po = keyDb.getPerspective(OFFICIAL);
        CommitObject cObj = objDb.getCommit(po.getLatestCommit());
        cObj = getCommitViaDirective(cObj, documentName, directive);

        if (cObj != null) {
            String ret = getDocumentFromCommit(documentName, cObj);
            return ret;
        } else {
            return null;
        }
    }

    public String getDocumentFromCommit(String documentName, CommitObject cObj) {
        TreeObject tObj = objDb.getTree(cObj.getTreeRef());
        String[] parts = documentName.split("/");
        String ret = null;
        for (int i = 0; i < parts.length; i++) {
            if (tObj == null) {
                break;
            }
            if (i == parts.length - 1) {
                for (DocumentBagReference bagRef : tObj.getDocuments()) {
                    DocumentBagObject dObj = objDb.getDocumentBag(bagRef.getBagRef());

                    ret = dObj.getDocRefs().get(parts[i]);
                    if (ret != null) {
                        ret = objDb.getDocument(ret).getContent().toString();
                        break;
                    }
                }
            } else {
                String treeRef = tObj.getTrees().get(parts[i]);
                tObj = objDb.getTree(treeRef);
            }
        }
        return ret;
    }

    @Override
    public DocumentObject getDocumentObject(String reference) {
        return objDb.getDocument(reference);
    }

    @Override
    public List<String> getDocuments(List<String> keys) {
        // Ideally use the batch method of the cacheStore
        List<String> docs = null;
        List<String> docsFromCache = cacheKeyStore.getBatch(keys);
        docs = new ArrayList<String>();
        // Now check for cache misses
        for (int i = 0; i < docsFromCache.size(); i++) {
            String d = docsFromCache.get(i);
            if (d == null) {
                d = getDocument(keys.get(i));
                if (d != null) {
                    log.info("Cache miss for " + keys.get(0) + " during batch operation");
                    cacheKeyStore.put(keys.get(i), d);
                }
            }
            docs.add(d);
        }
        return docs;
    }

    @Override
    // For each of the displaynames (document names)
    // Does that document exist in the perspective point?
    public boolean[] getExistence(List<String> displays) {
        boolean ret[] = new boolean[displays.size()];
        PerspectiveObject po = keyDb.getPerspective(OFFICIAL);
        CommitObject cObj = objDb.getCommit(po.getLatestCommit());
        TreeObject tObj = objDb.getTree(cObj.getTreeRef());
        List<String> displaysCopy = new ArrayList<String>();
        displaysCopy.addAll(displays);

        // Try this level
        String level = "";
        List<DocumentBagReference> refs = tObj.getDocuments();
        for (DocumentBagReference r : refs) {
            DocumentBagObject dObj = objDb.getDocumentBag(r.getBagRef());
            for (String key : dObj.getDocRefs().keySet()) {
                String testKey = level + key;
                if (displaysCopy.contains(testKey)) {
                    int pos = displays.indexOf(testKey);
                    ret[pos] = true;
                    displaysCopy.remove(testKey);
                    if (displaysCopy.isEmpty()) {
                        break;
                    }
                }
            }
            if (displaysCopy.isEmpty()) {
                break;
            }
        }

        // And now dive into another level
        if (displaysCopy.isEmpty()) {
            return ret;
        } else {
            for (Map.Entry<String, String> tree : tObj.getTrees().entrySet()) {
                String newEntry = level.isEmpty() ? tree.getKey() : level + "/" + tree.getKey();
                TreeObject innerObj = objDb.getTree(tree.getValue());
                ret = walkInnerExistence(newEntry, innerObj, displaysCopy, displays, ret);
                if (displaysCopy.isEmpty()) {
                    break;
                }
            }
        }

        return ret;
    }

    @Override
    public RaptureDNCursor getNextDNCursor(RaptureDNCursor cursor, int count) {
        throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_INTERNAL_ERROR, "Not yet implemented");
    }

    public ObjectDatabase getObjectDatabase() {
        return objDb;
    }

    private Stage getStage(String name) {
        Stage s = stages.get(name);
        if (s == null) {
            s = createStage(name);
        }
        return s;
    }

    @Override
    public String getTagDocument(String tag, String key) {
        TagObject to = keyDb.getTag(tag);
        CommitObject cObj = objDb.getCommit(to.getCommitRef());
        return getDocumentFromCommit(key, cObj);
    }

    @Override
    public List<String> getTags() {
        return keyDb.getTags();
    }

    @Override
    public TreeObject getTreeObject(String reference) {
        return objDb.getTree(reference);
    }

    private void handleCommentaryFromCommit(CommitObject cObj, String key, CommentaryHandler handler) {
        // Now visit the tree to find the document referenced by key
        TreeObject tObj = objDb.getTree(cObj.getTreeRef());
        String[] parts = key.split("/");
        String ultimateRef = null;
        for (int i = 0; i < parts.length; i++) {
            if (tObj == null) {
                break;
            }
            if (i == parts.length - 1) {
                for (DocumentBagReference bagRef : tObj.getDocuments()) {
                    DocumentBagObject dObj = objDb.getDocumentBag(bagRef.getBagRef());
                    ultimateRef = dObj.getDocRefs().get(parts[i]);
                    if (ultimateRef == null) {
                        ultimateRef = tObj.getTrees().get(parts[i]);
                        TreeObject content = objDb.getTree(ultimateRef);
                        if (content != null) {
                            if (handler.handleObject(content)) {
                                objDb.writeTree(content, ultimateRef);
                            }
                        }
                    } else {
                        DocumentObject content = objDb.getDocument(ultimateRef);
                        if (handler.handleObject(content)) {
                            objDb.writeDocument(content, ultimateRef);
                        }
                    }
                }
            } else {
                String treeRef = tObj.getTrees().get(parts[i]);
                tObj = objDb.getTree(treeRef);
            }
        }
    }

    /**
     * A test function really to setup a blank environment
     *
     * @
     */
    private void initBlankEnv() {
        // Create an initial commit with a null tree
        TreeObject tObj = new TreeObject();
        String ref = objDb.writeTree(tObj);
        CommitObject cObj = new CommitObject();
        cObj.setTreeRef(ref);
        cObj.setUser("admin");
        cObj.setWhen(new Date());
        cObj.setComment("Repo creation");
        String cRef = objDb.getCommitReference(cObj);
        cObj.setCommitRef(cRef);
        List<String> refs = new ArrayList<String>();
        refs.add(ref);
        cObj.setTreeReferences(refs);
        cObj.setDocReferences(new ArrayList<String>());
        objDb.writeCommit(cObj, cRef);

        PerspectiveObject main = new PerspectiveObject();
        main.setBaseCommit(cRef);
        main.setLatestCommit(cRef);
        main.setDescription("Main branch of work");
        main.setOwner(OFFICIAL);
        main.setWhen(new Date());

        keyDb.writePerspective(OFFICIAL, main);
        createStage(OFFICIAL);
    }

    private void initCacheStore() {
        // Basically copy the latest keys/values to the cacheKeyStore
        visitAll("", null, new RepoVisitor() {

            @Override
            public boolean visit(String name, JsonContent content, boolean isFolder) {
                if (!isFolder) {
                    System.out.println(name);
                    if (!cacheKeyStore.containsKey(name)) {
                        log.info("VCache initialization, writing " + name);
                        try {
                            cacheKeyStore.put(name, content.getContent());
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
                return true;
            }

        });
        cacheKeyStore.put(setupKey, "done");
    }

    public boolean isVersioned() {
        return true;
    }

    /**
     * Archives old versions
     *
     * @param versionLimit       number of versions to retain
     * @param timeLimit          commits older than timeLimit will be archived
     * @param ensureVersionLimit ensure number of versions to retain even if commit is older than timeLimit
     * @param user
     * @return
     */
    public boolean archiveRepoVersions(int versionLimit, long timeLimit, boolean ensureVersionLimit, String user) {
        if (versionLimit <= 0) {
            log.error("versionLimit should > 0");
            return false;
        }

        PerspectiveObject perspective = keyDb.getPerspective(OFFICIAL);
        CommitObject commitObj = objDb.getCommit(perspective.getLatestCommit());
        if (commitObj.getWhen().getTime() < timeLimit && !ensureVersionLimit) {
            throw RaptureExceptionFactory.create("Invalid archive - no version left");
        }
        List<String> keysToArchive = new ArrayList<String>();
        int versionsRemaining = versionLimit;
        while (commitObj != null) {
            // do not archive the very first commit: repo creation
            if (StringUtils.isEmpty(commitObj.getPreviousReference())) {
                break;
            }
            // archive commit if it's older than specified timestamp, and no ensureVersions
            boolean isCommitTooOld = commitObj.getWhen().getTime() < timeLimit && !ensureVersionLimit;
            if (versionsRemaining <= 0 || isCommitTooOld) {
                // archive commit, along with its docref and treerefs
                getKeysToArchive(commitObj, keysToArchive);
            } else {
                versionsRemaining--;
            }
            // move on to previous commit
            commitObj = objDb.getCommit(commitObj.getPreviousReference());
        }

        return objDb.delete(keysToArchive);
    }

    private void getKeysToArchive(CommitObject commitObj, List<String> keysToArchive) {
        keysToArchive.add(commitObj.getCommitRef());
        // add doc refs to archive
        keysToArchive.addAll(commitObj.getDocReferences());
        // add tree refs to archive
        for (String treeRef : commitObj.getTreeReferences()) {
            keysToArchive.add(treeRef);
            // add document bag ref to archive
            TreeObject treeObject = objDb.getTree(treeRef);
            List<DocumentBagReference> docBagRefs = treeObject.getDocuments();
            if (docBagRefs != null) {
                for (DocumentBagReference bagRef : docBagRefs) {
                    keysToArchive.add(bagRef.getBagRef());
                }
            }
        }
    }

    /**
     * Merge one perspective into another
     *
     * @param targetPerspetive
     * @param sourcePerspective
     * @throws Exception
     */
    public void mergePerspective(String targetPerspetive, String sourcePerspective) {
        // We need to look at the target and sourcePerspectives with respect to
        // their commits
        // We find out a common ancestor commit, and start a Stage from the
        // targetPerspective
        // We then continuously apply the treeObject from the common ancestor
        // point to the commit in the sourcePerspective
        // That final stage is the one we commit to move the targetPerspective
        // forward.

        PerspectiveObject target = keyDb.getPerspective(targetPerspetive);
        PerspectiveObject source = keyDb.getPerspective(sourcePerspective);
        CommitObject co = objDb.getCommit(target.getLatestCommit());
        TreeObject to = objDb.getTree(co.getTreeRef());
        StageTree st = new StageTree(to, capacity);
        // Now apply each tree object associated with the commits between the
        // latest commit in source and the start point
        List<String> commitList = new ArrayList<String>();
        String commitRef = source.getLatestCommit();
        while (!commitRef.equals(source.getBaseCommit())) {
            CommitObject cotest = objDb.getCommit(commitRef);
            commitList.add(commitRef);
            commitRef = cotest.getPreviousReference();
        }
        // We now have a list of trees to apply (but in the wrong order)
        for (int i = commitList.size() - 1; i >= 0; i--) {
            CommitObject cob = objDb.getCommit(commitList.get(i));
            TreeObject apply = objDb.getTree(cob.getTreeRef());
            st.apply(this, apply, commitList.get(i));
        }
        // And now commit this stage
        CommitObject cObj = new CommitObject();
        cObj.setUser("merge");
        cObj.setWhen(new Date());
        cObj.setPreviousReference(target.getLatestCommit());
        String cRef = objDb.getCommitReference(cObj);
        cObj.setCommitRef(cRef);

        CommitCollector collector = new CommitCollector();
        String reference = st.commitStage(this, cRef, collector, new HashMap<DocumentObject, String>());
        cObj.setTreeRef(reference);
        cObj.setChanges(collector.toString());
        objDb.writeCommit(cObj, cRef);
        target.setLatestCommit(cRef);
        source.setLatestCommit(cRef);
        source.setBaseCommit(cRef);
        keyDb.writePerspective(targetPerspetive, target);
        keyDb.writePerspective(sourcePerspective, source);
    }

    private void queryForTree(TreeObject tObj, String indent) {
        for (DocumentBagReference bagRef : tObj.getDocuments()) {
            DocumentBagObject dObj = objDb.getDocumentBag(bagRef.getBagRef());
            for (Map.Entry<String, String> docEntries : dObj.getDocRefs().entrySet()) {
                log.info(indent + docEntries.getKey() + " - "
                        + objDb.getDocument(docEntries.getValue()).getContent().toString());
            }
        }
        for (Map.Entry<String, String> treeEntries : tObj.getTrees().entrySet()) {
            log.info(indent + treeEntries.getKey() + " -V-");
            TreeObject inner = objDb.getTree(treeEntries.getValue());
            queryForTree(inner, indent + "   ");
        }
    }

    /**
     * For now this just dumps out the perspective by walking the associated
     * tree
     *
     * @param perspective
     * @throws Exception
     */
    public void queryPerspective(String perspective) {
        PerspectiveObject po = keyDb.getPerspective(perspective);
        CommitObject cObj = objDb.getCommit(po.getLatestCommit());
        TreeObject tObj = objDb.getTree(cObj.getTreeRef());
        queryForTree(tObj, "");
    }

    @Override
    public boolean removeDocument(String key, String user, String comment) {
        // Need to reset stage
        boolean removed = false;

        if (!stages.containsKey(OFFICIAL)) {
            createStage(OFFICIAL);
        }
        removed = removeFromStage(OFFICIAL, key);
        if (removed) {
            commitStage(OFFICIAL, user, comment);
        }
        cacheKeyStore.delete(key);
        return true;
    }

    @Override
    public boolean removeFromStage(String stage, String path) {
        Stage st = getStage(stage);
        String[] p = path.split("/");
        LinkedList<String> parts = new LinkedList<String>();
        for (String a : p) {
            parts.add(a);
        }
        return st.getStageBase().removeFromStage(this, parts);
    }

    @Override
    public void removeTag(String tagName) {
        keyDb.deleteTag(tagName);
    }

    @Override
    public RaptureQueryResult runNativeQuery(String repoType, List<String> queryParams) {
        throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_INTERNAL_ERROR, ERROR_CANNOT_RUN);
    }

    @Override
    public RaptureNativeQueryResult runNativeQueryWithLimitAndBounds(String repoType, List<String> queryParams,
            int limit, int offset) {
        throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_INTERNAL_ERROR, ERROR_CANNOT_RUN);
    }

    @Override
    public void setRemote(String remote, String remoteAuthority) {
        // IF the perspective does not exist, throw, otherwise update the
        // perspective and
        // save it back
        PerspectiveObject po = keyDb.getPerspective(OFFICIAL);
        RemoteLink rl = new RemoteLink();
        rl.setAuthority(remoteAuthority);
        rl.setRemoteId(remote);
        rl.setPerspective(OFFICIAL);
        po.setRemoteLink(rl);
        keyDb.writePerspective(OFFICIAL, po);
    }

    @Override
    public void visitAll(String prefix, BaseDirective directive, RepoVisitor visitor) {
        PerspectiveObject po = keyDb.getPerspective(OFFICIAL);
        CommitObject cObj = objDb.getCommit(po.getLatestCommit());
        cObj = getCommitViaDirective(cObj, null, directive);
        TreeObject tObj = objDb.getTree(cObj.getTreeRef());
        // TODO: optimise this by finding the correct part of the tree based on
        // the prefix
        visitTree(tObj, prefix, visitor, "");
    }

    @Override
    public List<RaptureFolderInfo> getChildren(String displayNamePart) {
        return null;
    }

    @Override
    public List<RaptureFolderInfo> removeChildren(String area, Boolean force) {
        return null;
    }

    @Override
    public void visitFolder(String folder, BaseDirective directive, RepoVisitor visitor) {
        PerspectiveObject po = keyDb.getPerspective(OFFICIAL);
        CommitObject cObj = objDb.getCommit(po.getLatestCommit());
        cObj = getCommitViaDirective(cObj, null, directive);
        TreeObject tObj = objDb.getTree(cObj.getTreeRef());
        visitFolderFromTree(folder, visitor, tObj);
    }

    public void visitFolderFromTree(String folder, RepoVisitor visitor, TreeObject ptObj) {
        TreeObject tObj = ptObj;

        if (!folder.isEmpty()) {
            String[] parts = folder.split("/");
            for (String p : parts) {
                if (tObj.getTrees().containsKey(p)) {
                    tObj = objDb.getTree(tObj.getTrees().get(p));
                } else {
                    throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_INTERNAL_ERROR, "Not found");
                }
            }
        }
        // Now tObj is where we should be
        for (DocumentBagReference bagRef : tObj.getDocuments()) {
            DocumentBagObject dObj = objDb.getDocumentBag(bagRef.getBagRef());
            for (Map.Entry<String, String> docEntries : dObj.getDocRefs().entrySet()) {
                if (!docEntries.getKey().isEmpty()) {
                    visitor.visit(docEntries.getKey(), objDb.getDocument(docEntries.getValue()).getContent(),
                            false);
                }
            }
        }
        for (Map.Entry<String, String> folderEntries : tObj.getTrees().entrySet()) {
            visitor.visit(folderEntries.getKey(), null, true);
        }
    }

    @Override
    public void visitFolders(String folderPrefix, BaseDirective directive, final RepoFolderVisitor visitor) {
        // Visit just the folders, that match this prefix
        if (directive == null) {
            cacheKeyStore.visit(folderPrefix, new RepoVisitor() {
                @Override
                public boolean visit(String name, JsonContent content, boolean isFolder) {
                    visitor.folder(name);
                    return true;
                }
            });
            return;
        }
        PerspectiveObject po = keyDb.getPerspective(OFFICIAL);
        CommitObject cObj = objDb.getCommit(po.getLatestCommit());
        cObj = getCommitViaDirective(cObj, null, directive);
        TreeObject tObj = objDb.getTree(cObj.getTreeRef());
        visitFolderFromTree(folderPrefix, new RepoVisitor() {

            @Override
            public boolean visit(String name, JsonContent content, boolean isFolder) {
                visitor.folder(name);
                return true;
            }

        }, tObj);
    }

    @Override
    public void visitTag(String tagName, String prefix, RepoVisitor visitor) {
        TagObject to = keyDb.getTag(tagName);
        CommitObject cObj = objDb.getCommit(to.getCommitRef());
        TreeObject tObj = objDb.getTree(cObj.getTreeRef());
        visitTree(tObj, prefix, visitor, "");
    }

    @Override
    public void visitTagFolder(String tagName, String folder, RepoVisitor visitor) {
        TagObject to = keyDb.getTag(tagName);
        CommitObject cObj = objDb.getCommit(to.getCommitRef());
        TreeObject tObj = objDb.getTree(cObj.getTreeRef());
        visitFolderFromTree(folder, visitor, tObj);
    }

    private boolean visitTree(TreeObject tObj, String prefix, RepoVisitor visitor, String treePrefix) {
        // Visit docs first

        for (DocumentBagReference bagRef : tObj.getDocuments()) {
            DocumentBagObject dObj = objDb.getDocumentBag(bagRef.getBagRef());
            for (Map.Entry<String, String> docEntries : dObj.getDocRefs().entrySet()) {
                if (treePrefix.startsWith(prefix)) {
                    // If we cannot visit a document, (i.e. an exception is
                    // thrown, continue)
                    try {
                        if (!visitor.visit(treePrefix + docEntries.getKey(),
                                objDb.getDocument(docEntries.getValue()).getContent(), false)) {
                            return false;
                        }
                    } catch (RaptureException e) {
                        log.error("Could not decode document " + e.getMessage());
                    }
                }
            }
        }
        for (Map.Entry<String, String> treeEntries : tObj.getTrees().entrySet()) {
            TreeObject inner = objDb.getTree(treeEntries.getValue());
            if (!visitTree(inner, prefix, visitor, treePrefix + treeEntries.getKey() + "/")) {
                return false;
            }
        }
        return true;
    }

    private boolean[] walkInnerExistence(String level, TreeObject tObj, List<String> displaysCopy,
            List<String> displays, boolean[] ret) {
        List<DocumentBagReference> refs = tObj.getDocuments();
        for (DocumentBagReference r : refs) {
            DocumentBagObject dObj = objDb.getDocumentBag(r.getBagRef());
            for (String key : dObj.getDocRefs().keySet()) {
                String testKey = level.isEmpty() ? key : level + "/" + key;
                if (displaysCopy.contains(testKey)) {
                    int pos = displays.indexOf(testKey);
                    ret[pos] = true;
                    displaysCopy.remove(testKey);
                    if (displaysCopy.isEmpty()) {
                        break;
                    }
                }
            }
            if (displaysCopy.isEmpty()) {
                break;
            }
        }

        // And now dive into another level
        if (displaysCopy.isEmpty()) {
            return ret;
        } else {
            for (Map.Entry<String, String> tree : tObj.getTrees().entrySet()) {
                String newEntry = level + "/" + tree.getKey();
                TreeObject innerObj = objDb.getTree(tree.getValue());
                ret = walkInnerExistence(newEntry, innerObj, displaysCopy, displays, ret);
                if (displaysCopy.isEmpty()) {
                    break;
                }
            }
        }
        return ret;
    }

    @Override
    public void writeCommitObject(String reference, CommitObject commit) {
        objDb.writeCommit(commit, reference);
    }

    @Override
    public void writeDocumentObject(String reference, DocumentObject docObject) {
        objDb.writeDocument(docObject, reference);
    }

    @Override
    public void writeTreeObject(String reference, TreeObject treeObject) {
        objDb.writeTree(treeObject, reference);
    }

    @Override
    public DocumentWithMeta addDocumentWithVersion(String disp, String content, String user, String comment,
            boolean mustBeNew, int expectedVersion) {
        return addDocument(disp, content, user, comment, mustBeNew);
    }

    @Override
    public boolean hasMetaContent() {
        return false;
    }

    @Override
    public List<DocumentVersionInfo> getVersionHistory(String key) {
        throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_INTERNAL_ERROR,
                Messages.getString("BaseSimpleRepo.notsupp")); //$NON-NLS-1$      

    }

    @Override
    public List<DocumentWithMeta> getVersionMeta(String key, List<Integer> versions) {
        throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_INTERNAL_ERROR,
                Messages.getString("BaseSimpleRepo.notsupp")); //$NON-NLS-1$      

    }

    @Override
    public List<Boolean> removeVersionMeta(String key, List<Integer> versions) {
        throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_INTERNAL_ERROR,
                Messages.getString("BaseSimpleRepo.notsupp")); //$NON-NLS-1$      

    }

    @Override
    public void setDocAttribute(RaptureURI uri, DocumentAttribute attribute) {
        throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_INTERNAL_ERROR,
                Messages.getString("BaseSimpleRepo.notsupp")); //$NON-NLS-1$              
    }

    @Override
    public DocumentAttribute getDocAttribute(RaptureURI uri) {
        throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_INTERNAL_ERROR,
                Messages.getString("BaseSimpleRepo.notsupp")); //$NON-NLS-1$      
    }

    @Override
    public List<DocumentAttribute> getDocAttributes(RaptureURI uri) {
        throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_INTERNAL_ERROR,
                Messages.getString("BaseSimpleRepo.notsupp")); //$NON-NLS-1$      
    }

    @Override
    public Boolean deleteDocAttribute(RaptureURI uri) {
        throw RaptureExceptionFactory.create(HttpURLConnection.HTTP_INTERNAL_ERROR,
                Messages.getString("BaseSimpleRepo.notsupp")); //$NON-NLS-1$
    }

    @Override
    public Boolean validate() {
        return store.validate();
    }

    @Override
    public Optional<IndexHandler> getIndexHandler() {
        return Optional.absent();
    }

    @Override
    public DocumentWithMeta addTagToDocument(String user, String docPath, String tagUri, String value) {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public DocumentWithMeta addTagsToDocument(String user, String docPath, Map<String, String> tagMap) {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public DocumentWithMeta removeTagFromDocument(String user, String docPath, String tagUri) {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public DocumentWithMeta removeTagsFromDocument(String user, String docPath, List<String> tags) {
        // TODO Auto-generated method stub
        return null;
    }
}