org.locationtech.geogig.repository.impl.WorkingTreeImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.locationtech.geogig.repository.impl.WorkingTreeImpl.java

Source

/* Copyright (c) 2012-2016 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.repository.impl;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.locationtech.geogig.model.RevTree.EMPTY;
import static org.locationtech.geogig.model.RevTree.EMPTY_TREE_ID;

import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;

import org.eclipse.jdt.annotation.Nullable;
import org.locationtech.geogig.data.FindFeatureTypeTrees;
import org.locationtech.geogig.model.Node;
import org.locationtech.geogig.model.ObjectId;
import org.locationtech.geogig.model.Ref;
import org.locationtech.geogig.model.RevFeature;
import org.locationtech.geogig.model.RevFeatureType;
import org.locationtech.geogig.model.RevObject.TYPE;
import org.locationtech.geogig.model.RevTree;
import org.locationtech.geogig.model.impl.CanonicalTreeBuilder;
import org.locationtech.geogig.model.impl.RevFeatureTypeBuilder;
import org.locationtech.geogig.model.impl.RevTreeBuilder;
import org.locationtech.geogig.plumbing.DiffCount;
import org.locationtech.geogig.plumbing.DiffWorkTree;
import org.locationtech.geogig.plumbing.FindOrCreateSubtree;
import org.locationtech.geogig.plumbing.FindTreeChild;
import org.locationtech.geogig.plumbing.LsTreeOp;
import org.locationtech.geogig.plumbing.LsTreeOp.Strategy;
import org.locationtech.geogig.plumbing.ResolveTreeish;
import org.locationtech.geogig.plumbing.UpdateRef;
import org.locationtech.geogig.plumbing.UpdateTree;
import org.locationtech.geogig.repository.AutoCloseableIterator;
import org.locationtech.geogig.repository.Context;
import org.locationtech.geogig.repository.DefaultProgressListener;
import org.locationtech.geogig.repository.DiffEntry;
import org.locationtech.geogig.repository.DiffObjectCount;
import org.locationtech.geogig.repository.FeatureInfo;
import org.locationtech.geogig.repository.NodeRef;
import org.locationtech.geogig.repository.ProgressListener;
import org.locationtech.geogig.repository.Repository;
import org.locationtech.geogig.repository.WorkingTree;
import org.locationtech.geogig.storage.ObjectDatabase;
import org.opengis.feature.type.FeatureType;

import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicates;
import com.google.common.base.Stopwatch;
import com.google.common.collect.Iterators;
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.inject.Inject;
import com.vividsolutions.jts.geom.Envelope;

/**
 * A working tree is the collection of Features for a single FeatureType in GeoServer that has a
 * repository associated with it (and hence is subject of synchronization).
 * <p>
 * It represents the set of Features tracked by some kind of geospatial data repository (like the
 * GeoServer Catalog). It is essentially a "tree" with various roots and only one level of nesting,
 * since the FeatureTypes held in this working tree are the equivalents of files in a git working
 * tree.
 * </p>
 * <p>
 * <ul>
 * <li>A WorkingTree represents the current working copy of the versioned feature types
 * <li>A WorkingTree has a Repository
 * <li>A Repository holds commits and branches
 * <li>You perform work on the working tree (insert/delete/update features)
 * <li>Then you commit to the current Repository's branch
 * <li>You can checkout a different branch from the Repository and the working tree will be updated
 * to reflect the state of that branch
 * </ul>
 * 
 * @see Repository
 */
public class WorkingTreeImpl implements WorkingTree {

    private ObjectDatabase indexDatabase;

    private Context context;

    @Inject
    public WorkingTreeImpl(final Context injector) {
        this.indexDatabase = injector.objectDatabase();
        this.context = injector;
    }

    /**
     * Updates the WORK_HEAD ref to the specified tree.
     * 
     * @param newTree the tree to be set as the new WORK_HEAD
     */
    @Override
    public synchronized ObjectId updateWorkHead(ObjectId newTree) {
        context.command(UpdateRef.class).setName(Ref.WORK_HEAD).setNewValue(newTree).call();
        return newTree;
    }

    /**
     * @return the tree represented by WORK_HEAD. If there is no tree set at WORK_HEAD, it will
     *         return the HEAD tree (no unstaged changes).
     */
    @Override
    public synchronized RevTree getTree() {
        Optional<ObjectId> workTreeId = context.command(ResolveTreeish.class).setTreeish(Ref.WORK_HEAD).call();

        RevTree workTree = EMPTY;

        if (workTreeId.isPresent()) {
            if (!workTreeId.get().equals(EMPTY_TREE_ID)) {
                workTree = indexDatabase.getTree(workTreeId.get());
            }
        } else {
            // Work tree was not resolved, update it to the head.
            Optional<ObjectId> headTreeId = context.command(ResolveTreeish.class).setTreeish(Ref.HEAD).call();

            if (headTreeId.isPresent() && !headTreeId.get().equals(EMPTY_TREE_ID)) {
                workTree = context.objectDatabase().getTree(headTreeId.get());
                updateWorkHead(workTree.getId());
            }
        }
        Preconditions.checkState(workTree != null);
        return workTree;
    }

    /**
     * Deletes a single feature from the working tree and updates the WORK_HEAD ref.
     * 
     * @param parentPath the parent path of the feature
     * @param featureId the id of the feature
     * @return true if the object was found and deleted, false otherwise
     */
    @Override
    public boolean delete(final String parentPath, final String featureId) {
        final RevTree workHead = getTree();
        final ObjectId newWorkHeadId = delete(NodeRef.appendChild(parentPath, featureId));
        return !workHead.getId().equals(newWorkHeadId);
    }

    /**
     * @param path the path to the tree to truncate
     * @return the new {@link ObjectId} for the root tree in the {@link Ref#WORK_HEAD working tree}
     */
    @Override
    public ObjectId truncate(final String path) {
        final RevTree workHead = getTree();

        final NodeRef currentTypeRef = context.command(FindTreeChild.class).setParent(workHead).setChildPath(path)
                .call().orNull();

        if (null == currentTypeRef) {
            return workHead.getId();
        }
        checkArgument(TYPE.TREE.equals(currentTypeRef.getType()), "%s is not a tree: %s", path,
                currentTypeRef.getType());

        final NodeRef newTypeRef = currentTypeRef.update(EMPTY_TREE_ID, (Envelope) null);

        final RevTree newWorkHead = context.command(UpdateTree.class).setRoot(workHead).setChild(newTypeRef).call();
        if (!workHead.equals(newWorkHead)) {
            updateWorkHead(newWorkHead.getId());
        }
        return newWorkHead.getId();
    }

    /**
     * Deletes a tree and the features it contains from the working tree and updates the WORK_HEAD
     * ref.
     * <p>
     * Note this methods completely removes the tree from the working tree. If the tree pointed out
     * to by {@code path} should be left empty, use {@link #truncate} instead.
     * 
     * @param treePath the path to the tree to delete
     * @return
     * @throws Exception
     */
    @Override
    public ObjectId delete(final String treePath) {
        final RevTree workHead = getTree();

        final NodeRef childRef = context.command(FindTreeChild.class).setParent(workHead).setChildPath(treePath)
                .call().orNull();

        if (null == childRef) {
            return workHead.getId();
        }
        final RevTree newWorkTree;
        if (TYPE.FEATURE.equals(childRef.getType())) {
            final String featureId = childRef.name();
            final String parentTreePath = childRef.getParentPath();
            final NodeRef typeTreeRef = context.command(FindTreeChild.class).setParent(workHead)
                    .setChildPath(parentTreePath).call().get();
            final RevTree currentParent = indexDatabase.getTree(typeTreeRef.getObjectId());
            CanonicalTreeBuilder parentBuilder = CanonicalTreeBuilder.create(indexDatabase, currentParent);
            parentBuilder.remove(featureId);

            final RevTree newParent = parentBuilder.build();
            if (newParent.getId().equals(typeTreeRef.getObjectId())) {
                return workHead.getId();
            }
            final Envelope newBounds = SpatialOps.boundsOf(newParent);
            final NodeRef newParentRef = typeTreeRef.update(newParent.getId(), newBounds);
            newWorkTree = context.command(UpdateTree.class).setRoot(workHead).setChild(newParentRef).call();
        } else {
            checkArgument(TYPE.TREE.equals(childRef.getType()));
            newWorkTree = context.command(UpdateTree.class).setRoot(workHead).removeChildTree(treePath).call();
        }

        if (!workHead.equals(newWorkTree)) {
            updateWorkHead(newWorkTree.getId());
        }
        return newWorkTree.getId();
    }

    @Override
    public ObjectId delete(Iterator<String> features, ProgressListener progress) {

        final ExecutorService treeBuildingService = Executors.newSingleThreadExecutor(
                new ThreadFactoryBuilder().setNameFormat("WorkingTree-tree-builder-%d").build());

        try {
            final WorkingTreeInsertHelper insertHelper;
            final RevTree currewntWorkHead = getTree();

            insertHelper = new WorkingTreeInsertHelper(context, currewntWorkHead, treeBuildingService);

            while (features.hasNext() && !progress.isCanceled()) {
                String featurePath = features.next();
                insertHelper.remove(featurePath);
            }
            if (progress.isCanceled()) {
                return currewntWorkHead.getId();
            }

            final Map<NodeRef, RevTree> trees = insertHelper.buildTrees();

            UpdateTree updateTree = context.command(UpdateTree.class).setRoot(currewntWorkHead);

            for (Map.Entry<NodeRef, RevTree> treeEntry : trees.entrySet()) {
                if (progress.isCanceled()) {
                    return currewntWorkHead.getId();
                }
                NodeRef treeRef = treeEntry.getKey();
                assert indexDatabase.exists(treeRef.getObjectId());
                updateTree.setChild(treeRef);
            }

            final RevTree newWorkHead = updateTree.call();

            if (!newWorkHead.equals(currewntWorkHead) && !progress.isCanceled()) {
                updateWorkHead(newWorkHead.getId());
            }

            return newWorkHead.getId();
        } finally {
            treeBuildingService.shutdownNow();
        }
    }

    @Override
    public synchronized NodeRef createTypeTree(final String treePath, final FeatureType featureType) {

        NodeRef.checkValidPath(treePath);

        final RevTree workHead = getTree();
        final NodeRef currentTreeRef = context.command(FindTreeChild.class).setParent(workHead)
                .setChildPath(treePath).call().orNull();
        if (null != currentTreeRef) {
            throw new IllegalArgumentException("Tree already exists at " + treePath);
        }

        final RevFeatureType revType = RevFeatureTypeBuilder.build(featureType);
        indexDatabase.put(revType);

        final ObjectId metadataId = revType.getId();

        final String parentPath = NodeRef.parentPath(treePath);
        final String treeName = NodeRef.nodeFromPath(treePath);

        final NodeRef newTreeRef = NodeRef.create(parentPath, Node.tree(treeName, EMPTY_TREE_ID, metadataId));

        final RevTree newWorkHead = context.command(UpdateTree.class).setRoot(workHead).setChild(newTreeRef).call();

        updateWorkHead(newWorkHead.getId());

        NodeRef ref = context.command(FindTreeChild.class).setParent(newWorkHead).setChildPath(treePath).call()
                .orNull();
        checkNotNull(ref, "tree wasn't created: " + treePath);
        return ref;
    }

    @Override
    public ObjectId insert(FeatureInfo featureInfo) {
        checkNotNull(featureInfo);
        return insert(Iterators.singletonIterator(featureInfo), DefaultProgressListener.NULL);
    }

    @Override
    public ObjectId insert(Iterator<FeatureInfo> featureInfos, ProgressListener progress) {
        checkArgument(featureInfos != null);
        checkArgument(progress != null);

        final RevTree currentWorkHead = getTree();
        final Map<String, NodeRef> currentTrees = Maps
                .newHashMap(Maps.uniqueIndex(getFeatureTypeTrees(), (nr) -> nr.path()));

        Map<String, CanonicalTreeBuilder> parentBuilders = new HashMap<>();

        progress.setProgress(0);
        final AtomicLong p = new AtomicLong();
        Function<FeatureInfo, RevFeature> treeBuildingTransformer = (fi) -> {
            final String parentPath = NodeRef.parentPath(fi.getPath());
            final String fid = NodeRef.nodeFromPath(fi.getPath());
            @Nullable
            ObjectId metadataId = fi.getFeatureTypeId();
            CanonicalTreeBuilder parentBuilder = getTreeBuilder(currentTrees, parentBuilders, parentPath,
                    metadataId);

            if (fi.isDelete()) {
                if (parentBuilder != null) {
                    parentBuilder.remove(fid);
                }
                return null;
            }

            Preconditions.checkState(parentBuilder != null);
            RevFeature feature = fi.getFeature();
            NodeRef parentRef = currentTrees.get(parentPath);
            Preconditions.checkNotNull(parentRef);
            if (fi.getFeatureTypeId().equals(parentRef.getMetadataId())) {
                metadataId = ObjectId.NULL;// use the parent's default
            }

            ObjectId oid = feature.getId();
            Envelope bounds = SpatialOps.boundsOf(feature);
            Node featureNode = Node.create(fid, oid, metadataId, TYPE.FEATURE, bounds);

            parentBuilder.put(featureNode);

            progress.setProgress(p.incrementAndGet());
            return feature;
        };

        Iterator<RevFeature> features = Iterators.transform(featureInfos, treeBuildingTransformer);
        features = Iterators.filter(features, Predicates.notNull());
        features = Iterators.filter(features, (f) -> !progress.isCanceled());

        Stopwatch insertTime = Stopwatch.createStarted();
        indexDatabase.putAll(features);
        insertTime.stop();
        if (progress.isCanceled()) {
            return currentWorkHead.getId();
        }

        progress.setDescription(String.format("%,d features inserted in %s", p.get(), insertTime));

        UpdateTree updateTree = context.command(UpdateTree.class).setRoot(currentWorkHead);
        parentBuilders.forEach((path, builder) -> {

            final NodeRef oldTreeRef = currentTrees.get(path);
            progress.setDescription(String.format("Building final tree %s...", oldTreeRef.name()));
            Stopwatch treeTime = Stopwatch.createStarted();
            final RevTree newFeatureTree = builder.build();
            treeTime.stop();
            progress.setDescription(
                    String.format("%,d features tree built in %s", newFeatureTree.size(), treeTime));
            final NodeRef newTreeRef = oldTreeRef.update(newFeatureTree.getId(),
                    SpatialOps.boundsOf(newFeatureTree));
            updateTree.setChild(newTreeRef);
        });

        final RevTree newWorkHead = updateTree.call();
        return updateWorkHead(newWorkHead.getId());
    }

    @Nullable
    private CanonicalTreeBuilder getTreeBuilder(final Map<String, NodeRef> currentTrees,
            final Map<String, CanonicalTreeBuilder> treeBuilders, final String treePath,
            final @Nullable ObjectId featureMetadataId) {

        checkNotNull(treePath);
        CanonicalTreeBuilder builder = treeBuilders.get(treePath);
        if (builder == null) {
            NodeRef treeRef = currentTrees.get(treePath);
            if (treeRef == null) {
                if (featureMetadataId == null) {
                    return null;
                }
                String parentPath = NodeRef.parentPath(treePath);
                String name = NodeRef.nodeFromPath(treePath);
                Node treeNode = Node.create(name, EMPTY_TREE_ID, featureMetadataId, TYPE.TREE, null);
                treeRef = new NodeRef(treeNode, parentPath, featureMetadataId);
                currentTrees.put(treePath, treeRef);
            }
            builder = CanonicalTreeBuilder.create(indexDatabase,
                    context.command(FindOrCreateSubtree.class).setParent(getTree()).setChildPath(treePath).call());
            treeBuilders.put(treePath, builder);
        }
        return builder;
    }

    /**
     * Determines if a specific feature type is versioned (existing in the main repository).
     * 
     * @param treePath feature type to check
     * @return true if the feature type is versioned, false otherwise.
     */
    @Override
    public boolean hasRoot(final String treePath) {

        Optional<NodeRef> typeNameTreeRef = context.command(FindTreeChild.class).setChildPath(treePath).call();

        return typeNameTreeRef.isPresent();
    }

    /**
     * @param pathFilter if specified, only changes that match the filter will be returned
     * @return an iterator for all of the differences between the work tree and the index based on
     *         the path filter.
     */
    @Override
    public AutoCloseableIterator<DiffEntry> getUnstaged(final @Nullable String pathFilter) {
        AutoCloseableIterator<DiffEntry> unstaged = context.command(DiffWorkTree.class).setFilter(pathFilter)
                .setReportTrees(true).call();
        return unstaged;
    }

    /**
     * @param pathFilter if specified, only changes that match the filter will be counted
     * @return the number differences between the work tree and the index based on the path filter.
     */
    @Override
    public DiffObjectCount countUnstaged(final @Nullable String pathFilter) {
        DiffObjectCount count = context.command(DiffCount.class).setOldVersion(Ref.STAGE_HEAD)
                .setNewVersion(Ref.WORK_HEAD).addFilter(pathFilter).call();
        return count;
    }

    /**
     * Returns true if there are no unstaged changes, false otherwise
     */
    @Override
    public boolean isClean() {
        Optional<ObjectId> resolved = context.command(ResolveTreeish.class).setTreeish(Ref.STAGE_HEAD).call();
        return getTree().getId().equals(resolved.or(ObjectId.NULL));
    }

    /**
     * @param path finds a {@link Node} for the feature at the given path in the index
     * @return the Node for the feature at the specified path if it exists in the work tree,
     *         otherwise Optional.absent()
     */
    @Override
    public Optional<Node> findUnstaged(final String path) {
        Optional<NodeRef> nodeRef = context.command(FindTreeChild.class).setParent(getTree()).setChildPath(path)
                .call();
        if (nodeRef.isPresent()) {
            return Optional.of(nodeRef.get().getNode());
        } else {
            return Optional.absent();
        }
    }

    /**
     * @return a list of all the feature type names in the working tree
     * @see FindFeatureTypeTrees
     */
    @Override
    public List<NodeRef> getFeatureTypeTrees() {

        List<NodeRef> typeTrees = context.command(FindFeatureTypeTrees.class).setRootTreeRef(Ref.WORK_HEAD).call();
        return typeTrees;
    }

    /**
     * Updates the definition of a Feature type associated as default feature type to a given path.
     * <p>
     * It also modifies the metadataId associated to feature nodes under the passed path, which used
     * the previous default feature type.
     * 
     * @param path the path
     * @param featureType the new feature type definition to set as default for the passed path
     */
    @Override
    public NodeRef updateTypeTree(final String treePath, final FeatureType featureType) {

        final RevTree workHead = getTree();

        final NodeRef typeTreeRef = context.command(FindTreeChild.class).setParent(workHead).setChildPath(treePath)
                .call().orNull();
        Preconditions.checkArgument(null != typeTreeRef, "Tree does not exist: %s", treePath);

        final RevFeatureType newRevType = RevFeatureTypeBuilder.build(featureType);
        if (newRevType.getId().equals(typeTreeRef.getMetadataId())) {
            return typeTreeRef;
        }
        indexDatabase.put(newRevType);

        final ObjectId oldDefaultMetadata = typeTreeRef.getDefaultMetadataId();
        final ObjectId newDeafultMetadata = newRevType.getId();

        Preconditions.checkState(!ObjectId.NULL.equals(oldDefaultMetadata));

        Iterator<NodeRef> oldFeatureRefs = context.command(LsTreeOp.class)//
                .setReference(treePath)//
                .setStrategy(Strategy.DEPTHFIRST_ONLY_FEATURES).call();

        // rebuild the tree with nodes with updated metadata id as necessary
        RevTreeBuilder newTreeBuilder = CanonicalTreeBuilder.create(indexDatabase);

        while (oldFeatureRefs.hasNext()) {
            NodeRef ref = oldFeatureRefs.next();
            Node node = ref.getNode();
            ObjectId newMetadataId;
            final @Nullable ObjectId nodesMetadataId = node.getMetadataId().orNull();
            if (null == nodesMetadataId) {
                // force the node holding the old metadata id instead of relying on its parent's
                newMetadataId = oldDefaultMetadata;
            } else if (newDeafultMetadata.equals(nodesMetadataId)) {
                // explicitly had the new metadataid, make it default to the new parent's
                newMetadataId = ObjectId.NULL;
            } else {
                // neither the old nor the new
                newMetadataId = nodesMetadataId;
            }
            Node newNode = Node.create(node.getName(), node.getObjectId(), newMetadataId, TYPE.FEATURE,
                    node.bounds().orNull());
            newTreeBuilder.put(newNode);
        }

        final RevTree newTypeTree = newTreeBuilder.build();

        final ObjectId overridingMetadataId = newRevType.getId();

        final Node overridingTreeNode = Node.tree(typeTreeRef.name(), newTypeTree.getId(), overridingMetadataId);

        final NodeRef newTreeRef = NodeRef.create(typeTreeRef.getParentPath(), overridingTreeNode);

        final RevTree newWorkHead = context.command(UpdateTree.class).setRoot(workHead).setChild(newTreeRef).call();

        updateWorkHead(newWorkHead.getId());

        return context.command(FindTreeChild.class).setParent(getTree()).setChildPath(treePath).call().get();

    }
}