Package org.locationtech.geogig.api.plumbing

Source Code of org.locationtech.geogig.api.plumbing.WriteTree2

/* Copyright (c) 2013-2014 Boundless and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Distribution License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/org/documents/edl-v10.html
*
* Contributors:
* Gabriel Roldan (Boundless) - initial implementation
*/
package org.locationtech.geogig.api.plumbing;

import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;

import javax.annotation.Nullable;

import org.locationtech.geogig.api.AbstractGeoGigOp;
import org.locationtech.geogig.api.Bounded;
import org.locationtech.geogig.api.Bucket;
import org.locationtech.geogig.api.Node;
import org.locationtech.geogig.api.NodeRef;
import org.locationtech.geogig.api.ObjectId;
import org.locationtech.geogig.api.ProgressListener;
import org.locationtech.geogig.api.Ref;
import org.locationtech.geogig.api.RevObject.TYPE;
import org.locationtech.geogig.api.RevTree;
import org.locationtech.geogig.api.RevTreeBuilder;
import org.locationtech.geogig.api.plumbing.LsTreeOp.Strategy;
import org.locationtech.geogig.api.plumbing.diff.DiffEntry;
import org.locationtech.geogig.api.plumbing.diff.MutableTree;
import org.locationtech.geogig.api.plumbing.diff.TreeDifference;
import org.locationtech.geogig.api.porcelain.CommitOp;
import org.locationtech.geogig.repository.SpatialOps;
import org.locationtech.geogig.storage.ObjectDatabase;
import org.locationtech.geogig.storage.StagingDatabase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.vividsolutions.jts.geom.Envelope;

/**
* Creates a new root tree in the {@link ObjectDatabase object database} from the current index,
* based on the current {@code HEAD} and returns the new root tree id.
* <p>
* <b>Note</b> this is a performance improvement replacement for {@link WriteTree} but so far is
* only used by {@link CommitOp}, as it doesn't have a proper replacement for
* {@link WriteTree#setDiffSupplier(Supplier)} yet, as used by the remote, REST, and WEB APIs.
* <p>
* This command creates a tree object using the current index. The id of the new root tree object is
* returned. No {@link Ref ref} is updated as a result of this operation, so the resulting root tree
* is "orphan". It's up to the calling code to update any needed reference.
*
* The index must be in a fully merged state.
*
* Conceptually, write-tree sync()s the current index contents into a set of tree objects on the
* {@link ObjectDatabase}. In order to have that match what is actually in your directory right now,
* you need to have done a {@link UpdateIndex} phase before you did the write-tree.
*
* @see TreeDifference
* @see MutableTree
* @see DeepMove
*/
public class WriteTree2 extends AbstractGeoGigOp<ObjectId> {

    private static final Logger LOGGER = LoggerFactory.getLogger(WriteTree2.class);

    private Supplier<RevTree> oldRoot;

    private final List<String> pathFilters = Lists.newLinkedList();

    // to be used when implementing a replacement for the current WriteTree2.setDiffSupplier()
    // private Supplier<Iterator<DiffEntry>> diffSupplier = null;

    /**
     * @param oldRoot a supplier for the old root tree
     * @return {@code this}
     */
    public WriteTree2 setOldRoot(Supplier<RevTree> oldRoot) {
        this.oldRoot = oldRoot;
        return this;
    }

    /**
     *
     * @param pathFilter the pathfilter to pass on to the index
     * @return {@code this}
     */
    public WriteTree2 addPathFilter(String pathFilter) {
        if (pathFilter != null) {
            this.pathFilters.add(pathFilter);
        }
        return this;
    }

    public WriteTree2 setPathFilter(@Nullable List<String> pathFilters) {
        this.pathFilters.clear();
        if (pathFilters != null) {
            this.pathFilters.addAll(pathFilters);
        }
        return this;
    }

    /**
     * Executes the write tree operation.
     *
     * @return the new root tree id, the current HEAD tree id if there are no differences between
     *         the index and the HEAD, or {@code null} if the operation has been cancelled (as
     *         indicated by the {@link #getProgressListener() progress listener}.
     */
    @Override
    protected ObjectId _call() {
        final ProgressListener progress = getProgressListener();

        TreeDifference treeDifference = computeTreeDifference();

        if (treeDifference.areEqual()) {
            MutableTree leftTree = treeDifference.getLeftTree();
            Node leftNode = leftTree.getNode();
            ObjectId leftOid = leftNode.getObjectId();
            return leftOid;
        }

        final MutableTree oldLeftTree = treeDifference.getLeftTree().clone();
        Preconditions.checkState(oldLeftTree.equals(treeDifference.getLeftTree()));

        // handle renames before new and deleted trees for the computation of new and deleted to be
        // accurate
        Set<String> ignoreList = Sets.newHashSet();
        handleRenames(treeDifference, ignoreList);
        handlePureMetadataChanges(treeDifference, ignoreList);
        handleNewTrees(treeDifference, ignoreList);
        handleDeletedTrees(treeDifference, ignoreList);
        handleRemainingDifferences(treeDifference, ignoreList);

        progress.complete();

        MutableTree newLeftTree = treeDifference.getLeftTree();

        final ObjectDatabase repositoryDatabase = objectDatabase();
        final RevTree newRoot = newLeftTree.build(stagingDatabase(), repositoryDatabase);
        if (newRoot.trees().isPresent()) {
            for (Node n : newRoot.trees().get()) {
                if (n.getMetadataId().isPresent()) deepMove(n.getMetadataId().get());
            }
        }

        ObjectId newRootId = newRoot.getId();

        return newRootId;
    }

    private void handlePureMetadataChanges(TreeDifference treeDifference, Set<String> ignoreList) {
        Map<NodeRef, NodeRef> pureMetadataChanges = treeDifference.findPureMetadataChanges();
        for (Map.Entry<NodeRef, NodeRef> e : pureMetadataChanges.entrySet()) {
            NodeRef newValue = e.getValue();
            String treePath = newValue.path();
            if (ignoreList.contains(treePath)) {
                continue;
            }
            ignoreList.add(treePath);
            if (!filterMatchesOrIsParent(treePath)) {
                continue;// filter doesn't apply to the changed tree
            }
            deepMove(newValue.getMetadataId());
            MutableTree leftTree = treeDifference.getLeftTree();
            leftTree.setChild(newValue.getParentPath(), newValue.getNode());
        }
    }

    private void handleDeletedTrees(TreeDifference treeDifference, Set<String> ignoreList) {
        SortedSet<NodeRef> deletes = treeDifference.findDeletes();
        for (NodeRef ref : deletes) {
            String path = ref.path();
            if (ignoreList.contains(path)) {
                continue;
            }
            ignoreList.add(path);
            if (!filterMatchesOrIsParent(path)) {
                if (filterApplies(path, treeDifference.getRightTree())) {
                    // can't optimize
                    RevTree newTree = applyChanges(ref, null);
                    Node newNode = Node.tree(ref.name(), newTree.getId(), ref.getMetadataId());
                    MutableTree leftTree = treeDifference.getLeftTree();
                    leftTree.forceChild(ref.getParentPath(), newNode);
                }
            } else {
                MutableTree leftTree = treeDifference.getLeftTree();
                leftTree.removeChild(path);
            }
        }
    }

    private void handleNewTrees(TreeDifference treeDifference, Set<String> ignoreList) {
        SortedSet<NodeRef> newTrees = treeDifference.findNewTrees();
        for (NodeRef ref : newTrees) {
            final String path = ref.path();
            if (ignoreList.contains(path)) {
                continue;
            }
            ignoreList.add(path);

            if (!filterMatchesOrIsParent(path)) {
                MutableTree rightTree = treeDifference.getRightTree();
                if (filterApplies(path, rightTree)) {
                    // can't optimize
                    RevTree newTree = applyChanges(null, ref);
                    Node newNode = Node.tree(ref.name(), newTree.getId(), ref.getMetadataId());
                    MutableTree leftTree = treeDifference.getLeftTree();
                    leftTree.forceChild(ref.getParentPath(), newNode);
                }
            } else {
                LOGGER.trace("Creating new tree {}", path);
                deepMove(ref.getNode());
                MutableTree leftTree = treeDifference.getLeftTree();
                String parentPath = ref.getParentPath();
                Node node = ref.getNode();
                leftTree.setChild(parentPath, node);
            }
        }
    }

    /**
     * A renamed tree is recognized by checking if a tree on the right points to the same object
     * that a tree on the left that doesn't exist anymore on the right.
     * <p>
     * Left entries are the original ones, and right entries are the new ones.
     * </p>
     *
     * @param treeDifference
     * @param ignoreList
     */
    private void handleRenames(TreeDifference treeDifference, Set<String> ignoreList) {
        final SortedMap<NodeRef, NodeRef> renames = treeDifference.findRenames();

        for (Map.Entry<NodeRef, NodeRef> e : renames.entrySet()) {
            NodeRef oldValue = e.getKey();
            NodeRef newValue = e.getValue();
            String newPath = newValue.path();
            if (ignoreList.contains(newPath)) {
                continue;
            }
            ignoreList.add(newPath);
            if (!filterMatchesOrIsParent(newPath)) {
                continue;// filter doesn't apply to the renamed tree as a whole
            }
            LOGGER.trace("Handling rename of {} as {}", oldValue.path(), newPath);
            MutableTree leftTree = treeDifference.getLeftTree();
            leftTree.removeChild(oldValue.path());
            leftTree.setChild(newValue.getParentPath(), newValue.getNode());
        }
    }

    private void handleRemainingDifferences(TreeDifference treeDifference, Set<String> ignoreList) {

        // old/new refs to trees that have changed and apply to the pathFilters, deepest paths first
        final SortedMap<NodeRef, NodeRef> changedTrees = treeDifference.findChanges();
        final SortedMap<NodeRef, NodeRef> filteredChangedTrees = changedTrees;// filterChanges(changedTrees);

        for (Map.Entry<NodeRef, NodeRef> changedTreeRefs : filteredChangedTrees.entrySet()) {

            NodeRef leftTreeRef = changedTreeRefs.getKey();
            NodeRef rightTreeRef = changedTreeRefs.getValue();
            String newPath = rightTreeRef.path();
            if (ignoreList.contains(newPath)) {
                continue;
            }
            if (!filterApplies(newPath, treeDifference.getRightTree())) {
                continue;
            }
            ignoreList.add(newPath);
            RevTree tree = applyChanges(leftTreeRef, rightTreeRef);

            Envelope bounds = SpatialOps.boundsOf(tree);
            Node newTreeNode = Node.create(rightTreeRef.name(), tree.getId(),
                    rightTreeRef.getMetadataId(), TYPE.TREE, bounds);

            MutableTree leftRoot = treeDifference.getLeftTree();
            String parentPath = rightTreeRef.getParentPath();
            leftRoot.setChild(parentPath, newTreeNode);
        }
    }

    private RevTree applyChanges(@Nullable final NodeRef leftTreeRef,
            @Nullable final NodeRef rightTreeRef) {

        Preconditions.checkArgument(leftTreeRef != null || rightTreeRef != null,
                "either left or right tree shall be non null");

        final ObjectDatabase repositoryDatabase = objectDatabase();
        final String treePath = rightTreeRef == null ? leftTreeRef.path() : rightTreeRef.path();

        final List<String> strippedPathFilters = stripParentAndFiltersThatDontApply(
                this.pathFilters, treePath);

        // find the diffs that apply to the path filters
        final ObjectId leftTreeId = leftTreeRef == null ? RevTree.EMPTY_TREE_ID : leftTreeRef
                .objectId();
        final ObjectId rightTreeId = rightTreeRef == null ? RevTree.EMPTY_TREE_ID : rightTreeRef
                .objectId();

        final Predicate<Bounded> existsFilter = new Predicate<Bounded>() {

            private final ObjectDatabase targetDb = repositoryDatabase;

            @Override
            public boolean apply(Bounded input) {
                ObjectId id = null;
                if (input instanceof Node && TYPE.TREE.equals(((Node) input).getType())) {
                    id = ((Node) input).getObjectId();
                } else if (input instanceof Bucket) {
                    Bucket b = (Bucket) input;
                    id = b.id();
                }
                if (id != null) {
                    if (targetDb.exists(id)) {
                        LOGGER.trace("Ignoring {}. Already exists in target database.", input);
                        return false;
                    }
                }
                return true;
            }
        };
        DiffTree diffs = command(DiffTree.class).setRecursive(false).setReportTrees(false)
                .setOldTree(leftTreeId).setNewTree(rightTreeId).setPathFilter(strippedPathFilters)
                .setCustomFilter(existsFilter);

        // move new blobs from the index to the repository (note: this could be parallelized)
        Supplier<Iterator<Node>> nodesToMove = asNodeSupplierOfNewContents(diffs,
                strippedPathFilters);
        command(DeepMove.class).setObjects(nodesToMove).call();

        final StagingDatabase stagingDatabase = stagingDatabase();

        final RevTree currentLeftTree = stagingDatabase.getTree(leftTreeId);

        final RevTreeBuilder builder = currentLeftTree.builder(repositoryDatabase);

        // remove the exists filter, we need to create the new trees taking into account all the
        // nodes
        diffs.setCustomFilter(null);
        Iterator<DiffEntry> iterator = diffs.get();
        if (!strippedPathFilters.isEmpty()) {
            final Set<String> expected = Sets.newHashSet(strippedPathFilters);
            iterator = Iterators.filter(iterator, new Predicate<DiffEntry>() {
                @Override
                public boolean apply(DiffEntry input) {
                    boolean applies;
                    if (input.isDelete()) {
                        applies = expected.contains(input.oldName());
                    } else {
                        applies = expected.contains(input.newName());
                    }
                    return applies;
                }
            });
        }

        for (; iterator.hasNext();) {
            final DiffEntry diff = iterator.next();
            if (diff.isDelete()) {
                builder.remove(diff.oldName());
            } else {
                NodeRef newObject = diff.getNewObject();
                Node node = newObject.getNode();
                builder.put(node);
            }
        }

        final RevTree newTree = builder.build();
        repositoryDatabase.put(newTree);
        return newTree;
    }

    private boolean filterMatchesOrIsParent(final String treePath) {
        if (pathFilters.isEmpty()) {
            return true;
        }

        for (String filter : pathFilters) {
            if (filter.equals(treePath)) {
                return true;
            }
            boolean treeIsChildOfFilter = NodeRef.isChild(filter, treePath);
            if (treeIsChildOfFilter) {
                return true;
            }
        }
        return false;
    }

    /**
     * Determines if any of the {@link #setPathFilter(List) path filters} apply to the given
     * {@code treePath}.
     * <p>
     * That is, whether the changes to the tree given by {@code treePath} should be processes.
     * <p>
     * A path filter applies to the given tree path if:
     * <ul>
     * <li>There are no filters at all
     * <li>A filter and the path are the same
     * <li>A filter is a child of the tree path (e.g. {@code filter = "roads/roads.0" and path =
     * "roads"})
     * <li>A filter is a parent of the tree given by {@code treePath} and addresses a tree instead
     * of a feature (e.g. {@code filter = "roads" and path = "roads/highways"}, but <b>not</b> if
     * {@code filter = "roads/roads.0" and path = "roads/highways"} where {@code roads/roads.0} is
     * not a tree as given by the tree structure in {@code rightTree} and hence may address a
     * feature that's a direct child of {@code roads} instead)
     * </ul>
     *
     * @param treePath a path to a tree in {@code rightTree}
     * @param rightTree the trees at the right side of the comparison, used to determine if a filter
     *        addresses a parent tree.
     * @return {@code true} if the changes in the tree given by {@code treePath} should be processed
     *         because any of the filters will match the changes on it
     */
    private boolean filterApplies(final String treePath, MutableTree rightTree) {
        if (pathFilters.isEmpty()) {
            return true;
        }

        final Set<String> childTrees = rightTree.getChildrenAsMap().keySet();

        for (String filter : pathFilters) {
            if (filter.equals(treePath)) {
                return true;
            }
            boolean filterIsChildOfTree = NodeRef.isDirectChild(treePath, filter);
            if (filterIsChildOfTree) {
                return true;
            }
            boolean filterIsParentOfTree = filterMatchesOrIsParent(treePath);
            boolean filterIsTree = childTrees.contains(filter);
            if (filterIsParentOfTree && filterIsTree) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return a new list out of the filters in pathFilters that apply to the given path (are equal
     *         or a parent of), with their own parents stripped to that they apply directly to the
     *         node names in the tree
     */
    private List<String> stripParentAndFiltersThatDontApply(List<String> pathFilters,
            final String treePath) {

        List<String> parentsStripped = Lists.newArrayListWithCapacity(pathFilters.size());
        for (String filter : pathFilters) {
            if (filter.equals(treePath)) {
                continue;// include all diffs in the tree addressed by treePath
            }
            boolean pathIsChildOfFilter = NodeRef.isChild(filter, treePath);
            if (pathIsChildOfFilter) {
                continue;// include all diffs in this path
            }

            boolean filterIsChildOfTree = NodeRef.isChild(treePath, filter);
            if (filterIsChildOfTree) {
                String filterFromPath = NodeRef.removeParent(treePath, filter);
                parentsStripped.add(filterFromPath);
            }
        }
        return parentsStripped;
    }

    /**
     * Transforms a {@code Supplier<DiffEntry>} to a {@code Supplier<Node>} with the
     * {@link DiffEntry#getNewObject() new nodes} of entries that represent changes or additions.
     *
     * @param strippedPathFilters
     */
    private Supplier<Iterator<Node>> asNodeSupplierOfNewContents(
            final Supplier<Iterator<DiffEntry>> supplier, final List<String> strippedPathFilters) {

        final Function<DiffEntry, Node> newNodes = new Function<DiffEntry, Node>() {
            @Override
            public Node apply(DiffEntry diffEntry) {
                return diffEntry.getNewObject().getNode();
            }
        };

        // filters DiffEntries that are not to be moved from index to objects (i.e. DELETE entries)
        final Predicate<DiffEntry> movableFilter = new Predicate<DiffEntry>() {
            final Set<String> expected = Sets.newHashSet(strippedPathFilters);

            @Override
            public boolean apply(DiffEntry e) {
                if (DiffEntry.ChangeType.REMOVED.equals(e.changeType())) {
                    return false;
                }
                if (!expected.isEmpty() && !expected.contains(e.newPath())) {
                    return false;
                }
                return true;
            }
        };

        return Suppliers.compose(new Function<Iterator<DiffEntry>, Iterator<Node>>() {
            @Override
            public Iterator<Node> apply(Iterator<DiffEntry> input) {
                Iterator<DiffEntry> onlyChanges = Iterators.filter(input, movableFilter);
                Iterator<Node> movableNodes = Iterators.transform(onlyChanges, newNodes);
                return movableNodes;
            }
        }, supplier);
    }

    private TreeDifference computeTreeDifference() {
        final String rightTreeish = Ref.STAGE_HEAD;

        final ObjectId rootTreeId = resolveRootTreeId();
        final ObjectId stageRootId = index().getTree().getId();

        final Supplier<Iterator<NodeRef>> leftTreeRefs;
        final Supplier<Iterator<NodeRef>> rightTreeRefs;
        if (rootTreeId.isNull()) {
            Iterator<NodeRef> empty = Iterators.emptyIterator();
            leftTreeRefs = Suppliers.ofInstance(empty);
        } else {
            leftTreeRefs = command(LsTreeOp.class).setReference(rootTreeId.toString()).setStrategy(
                    Strategy.DEPTHFIRST_ONLY_TREES);
        }
        rightTreeRefs = command(LsTreeOp.class).setReference(rightTreeish).setStrategy(
                Strategy.DEPTHFIRST_ONLY_TREES);

        MutableTree leftTree = MutableTree.createFromRefs(rootTreeId, leftTreeRefs);
        MutableTree rightTree = MutableTree.createFromRefs(stageRootId, rightTreeRefs);

        TreeDifference treeDifference = TreeDifference.create(leftTree, rightTree);
        return treeDifference;
    }

    @Nullable
    private NodeRef findTree(ObjectId objectId, Map<String, NodeRef> treeEntries) {
        for (NodeRef ref : treeEntries.values()) {
            if (objectId.equals(ref.objectId())) {
                return ref;
            }
        }
        return null;
    }

    private void deepMove(ObjectId object) {
        Supplier<ObjectId> objectRef = Suppliers.ofInstance(object);
        command(DeepMove.class).setObject(objectRef).setToIndex(false).call();
    }

    private void deepMove(Node ref) {
        Supplier<Node> objectRef = Suppliers.ofInstance(ref);
        command(DeepMove.class).setObjectRef(objectRef).setToIndex(false).call();
    }

    /**
     * @return the resolved root tree id
     */
    private ObjectId resolveRootTreeId() {
        if (oldRoot != null) {
            RevTree rootTree = oldRoot.get();
            return rootTree.getId();
        }
        ObjectId targetTreeId = command(ResolveTreeish.class).setTreeish(Ref.HEAD).call().get();
        return targetTreeId;
    }

}
TOP

Related Classes of org.locationtech.geogig.api.plumbing.WriteTree2

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.