/* Copyright (c) 2012-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 static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import org.geotools.geometry.jts.ReferencedEnvelope;
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.RevTree;
import org.locationtech.geogig.api.plumbing.diff.BoundsFilteringDiffConsumer;
import org.locationtech.geogig.api.plumbing.diff.DiffEntry;
import org.locationtech.geogig.api.plumbing.diff.DiffEntry.ChangeType;
import org.locationtech.geogig.api.plumbing.diff.DiffPathTracker;
import org.locationtech.geogig.api.plumbing.diff.PreOrderDiffWalk;
import org.locationtech.geogig.api.plumbing.diff.PreOrderDiffWalk.Consumer;
import org.locationtech.geogig.api.plumbing.diff.PreOrderDiffWalk.ForwardingConsumer;
import org.locationtech.geogig.api.plumbing.diff.PathFilteringDiffConsumer;
import org.locationtech.geogig.storage.ObjectDatabase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Supplier;
import com.google.common.base.Throwables;
import com.google.common.collect.AbstractIterator;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
/**
* Compares the content and metadata links of blobs found via two tree objects on the repository's
* {@link ObjectDatabase}
*/
public class DiffTree extends AbstractGeoGigOp<Iterator<DiffEntry>> implements
Supplier<Iterator<DiffEntry>> {
private static final Logger LOGGER = LoggerFactory.getLogger(DiffTree.class);
private final List<String> pathFilters = Lists.newLinkedList();
private ReferencedEnvelope boundsFilter;
private ChangeType changeTypeFilter;
private String oldRefSpec;
private String newRefSpec;
private boolean reportTrees;
private boolean recursive;
private Predicate<Bounded> customFilter;
/**
* Constructs a new instance of the {@code DiffTree} operation with the given parameters.
*/
public DiffTree() {
this.recursive = true;
}
/**
* @param oldRefSpec the ref that points to the "old" version
* @return {@code this}
*/
public DiffTree setOldVersion(String oldRefSpec) {
this.oldRefSpec = oldRefSpec;
return this;
}
/**
* @param newRefSpec the ref that points to the "new" version
* @return {@code this}
*/
public DiffTree setNewVersion(String newRefSpec) {
this.newRefSpec = newRefSpec;
return this;
}
/**
* @param oldTreeId the {@link ObjectId} of the "old" tree
* @return {@code this}
*/
public DiffTree setOldTree(ObjectId oldTreeId) {
this.oldRefSpec = oldTreeId.toString();
return this;
}
/**
* @param newTreeId the {@link ObjectId} of the "new" tree
* @return {@code this}
*/
public DiffTree setNewTree(ObjectId newTreeId) {
this.newRefSpec = newTreeId.toString();
return this;
}
/**
* @param path the path filter to use during the diff operation, replaces any other filter
* previously set
* @return {@code this}
*/
public DiffTree setPathFilter(@Nullable String path) {
if (path == null) {
setPathFilter((List<String>) null);
} else {
setPathFilter(ImmutableList.of(path));
}
return this;
}
public DiffTree setPathFilter(@Nullable List<String> pathFitlers) {
this.pathFilters.clear();
if (pathFitlers != null) {
this.pathFilters.addAll(pathFitlers);
}
return this;
}
public DiffTree setBoundsFilter(@Nullable ReferencedEnvelope bounds) {
this.boundsFilter = bounds;
return this;
}
public DiffTree setCustomFilter(@Nullable Predicate<Bounded> customFilter) {
this.customFilter = customFilter;
return this;
}
public DiffTree setChangeTypeFilter(@Nullable ChangeType changeType) {
this.changeTypeFilter = changeType;
return this;
}
/**
* Implements {@link Supplier#get()} by delegating to {@link #call()}.
*/
@Override
public Iterator<DiffEntry> get() {
return call();
}
/**
* Finds differences between the two specified trees.
*
* @return an iterator to a set of differences between the two trees
* @see DiffEntry
*/
@Override
protected Iterator<DiffEntry> _call() throws IllegalArgumentException {
checkNotNull(oldRefSpec, "old version not specified");
checkNotNull(newRefSpec, "new version not specified");
final RevTree oldTree = resolveTree(oldRefSpec);
final RevTree newTree = resolveTree(newRefSpec);
if (oldTree.equals(newTree)) {
return Iterators.emptyIterator();
}
ObjectDatabase leftSource = resolveSource(oldTree.getId());
ObjectDatabase rightSource = resolveSource(newTree.getId());
final PreOrderDiffWalk visitor = new PreOrderDiffWalk(oldTree, newTree, leftSource,
rightSource);
final BlockingQueue<DiffEntry> queue = new ArrayBlockingQueue<>(100);
final DiffEntryProducer diffProducer = new DiffEntryProducer(queue);
diffProducer.setReportTrees(this.reportTrees);
diffProducer.setRecursive(this.recursive);
final List<RuntimeException> producerErrors = new LinkedList<>();
Thread producerThread = new Thread("DiffTree producer thread") {
@Override
public void run() {
Consumer consumer = diffProducer;
if (customFilter != null) {// evaluated the latest
consumer = new PreOrderDiffWalk.FilteringConsumer(consumer, customFilter);
}
if (changeTypeFilter != null) {
consumer = new ChangeTypeFilteringDiffConsumer(changeTypeFilter, consumer);
}
if (boundsFilter != null) {
consumer = new BoundsFilteringDiffConsumer(boundsFilter, consumer,
stagingDatabase());
}
if (!pathFilters.isEmpty()) {// evaluated the former
consumer = new PathFilteringDiffConsumer(pathFilters, consumer);
}
try {
visitor.walk(consumer);
} catch (RuntimeException e) {
LOGGER.error("Error traversing diffs", e);
producerErrors.add(e);
} finally {
diffProducer.finished = true;
}
}
};
producerThread.setDaemon(true);
producerThread.start();
Iterator<DiffEntry> consumerIterator = new AbstractIterator<DiffEntry>() {
@Override
protected DiffEntry computeNext() {
if (!producerErrors.isEmpty()) {
throw new RuntimeException("Error in producer thread", producerErrors.get(0));
}
BlockingQueue<DiffEntry> entries = queue;
boolean finished = diffProducer.isFinished();
boolean empty = entries.isEmpty();
while (!finished || !empty) {
try {
DiffEntry entry = entries.poll(10, TimeUnit.MILLISECONDS);
if (entry != null) {
return entry;
}
finished = diffProducer.isFinished();
empty = entries.isEmpty();
} catch (InterruptedException e) {
throw Throwables.propagate(e);
}
}
return endOfData();
}
@Override
protected void finalize() {
diffProducer.finished = true;
}
};
return consumerIterator;
}
private RevTree resolveTree(final String treeIsh) {
RevTree tree;
if (treeIsh.equals(ObjectId.NULL.toString())) {
tree = RevTree.EMPTY;
} else {
final Optional<ObjectId> treeId = command(ResolveTreeish.class).setTreeish(treeIsh)
.call();
checkArgument(treeId.isPresent(), treeIsh + " did not resolve to a tree");
tree = command(RevObjectParse.class).setObjectId(treeId.get()).call(RevTree.class)
.or(RevTree.EMPTY);
}
return tree;
}
private ObjectDatabase resolveSource(ObjectId treeId) {
return objectDatabase().exists(treeId) ? objectDatabase() : stagingDatabase();
}
private static class ChangeTypeFilteringDiffConsumer extends ForwardingConsumer {
private final ChangeType changeTypeFilter;
public ChangeTypeFilteringDiffConsumer(ChangeType changeTypeFilter, Consumer consumer) {
super(consumer);
this.changeTypeFilter = changeTypeFilter;
}
@Override
public void feature(final Node left, final Node right) {
if (featureApplies(left, right)) {
super.feature(left, right);
}
}
@Override
public boolean tree(final Node left, final Node right) {
if (isRoot(left, right) || treeApplies(left, right)) {
return super.tree(left, right);
}
return false;
}
@Override
public void endTree(final Node left, final Node right) {
if (isRoot(left, right) || treeApplies(left, right)) {
super.endTree(left, right);
}
}
@Override
public boolean bucket(final int bucketIndex, final int bucketDepth, final Bucket left,
final Bucket right) {
return treeApplies(left, right) && super.bucket(bucketIndex, bucketDepth, left, right);
}
@Override
public void endBucket(int bucketIndex, int bucketDepth, Bucket left, Bucket right) {
if (treeApplies(left, right)) {
super.endBucket(bucketIndex, bucketDepth, left, right);
}
}
private boolean isRoot(final Node left, final Node right) {
return left == null ? right.getName().isEmpty() : left.getName().isEmpty();
}
private boolean featureApplies(final Node left, final Node right) {
switch (changeTypeFilter) {
case ADDED:
return left == null;
case MODIFIED:
return left != null && right != null;
case REMOVED:
return right == null;
default:
throw new IllegalArgumentException("Unknown change type: " + changeTypeFilter);
}
}
private boolean treeApplies(final Bounded left, final Bounded right) {
if (left != null && right != null) {
// if neither is null traversal of the trees must continue to figure out the
// differences
return true;
}
switch (changeTypeFilter) {
case ADDED:
return left == null;
case REMOVED:
return right == null;
case MODIFIED:
return false;// safe to return false as its guaranteed that either left or right is
// null
default:
throw new IllegalArgumentException("Unknown change type: " + changeTypeFilter);
}
}
}
private static class DiffEntryProducer implements Consumer {
private DiffPathTracker tracker = new DiffPathTracker();
private boolean reportFeatures = true, reportTrees = false;
private BlockingQueue<DiffEntry> entries;
private volatile boolean finished;
private boolean recursive = true;
public DiffEntryProducer(BlockingQueue<DiffEntry> queue) {
this.entries = queue;
}
@Override
public void feature(Node left, Node right) {
if (!finished && reportFeatures) {
String treePath = tracker.getCurrentPath();
NodeRef oldRef = left == null ? null : new NodeRef(left, treePath, tracker
.currentLeftMetadataId().or(ObjectId.NULL));
NodeRef newRef = right == null ? null : new NodeRef(right, treePath, tracker
.currentRightMetadataId().or(ObjectId.NULL));
try {
entries.put(new DiffEntry(oldRef, newRef));
} catch (InterruptedException e) {
// throw Throwables.propagate(e);
}
}
}
public void setRecursive(boolean recursive) {
this.recursive = recursive;
}
public void setReportTrees(boolean reportTrees) {
this.reportTrees = reportTrees;
}
public boolean isFinished() {
return finished;
}
@Override
public boolean tree(Node left, Node right) {
final String parentPath = tracker.getCurrentPath();
tracker.tree(left, right);
// System.err.printf("%s.tree(%s, %s)\n", getClass().getSimpleName(), left, right);
if (!finished && reportTrees) {
if (parentPath != null) {// do not report the root tree
NodeRef oldRef = left == null ? null : new NodeRef(left, parentPath, tracker
.currentLeftMetadataId().or(ObjectId.NULL));
NodeRef newRef = right == null ? null : new NodeRef(right, parentPath, tracker
.currentRightMetadataId().or(ObjectId.NULL));
try {
entries.put(new DiffEntry(oldRef, newRef));
} catch (InterruptedException e) {
// throw Throwables.propagate(e);
// die gracefully
return false;
}
}
}
if (recursive) {
return !finished;
}
return parentPath == null;
}
@Override
public void endTree(Node left, Node right) {
// System.err.printf("%s.endTree(%s, %s)\n", getClass().getSimpleName(), left, right);
tracker.endTree(left, right);
if (tracker.isEmpty()) {
finished = true;
}
}
@Override
public boolean bucket(int bucketIndex, int bucketDepth, Bucket left, Bucket right) {
return !finished;
}
@Override
public void endBucket(int bucketIndex, int bucketDepth, Bucket left, Bucket right) {
// no action required
}
}
/**
* @param reportTrees
* @return
*/
public DiffTree setReportTrees(boolean reportTrees) {
this.reportTrees = reportTrees;
return this;
}
/**
* Sets whether to return differences recursively ({@code true} or just for direct children (
* {@code false}. Defaults to {@code true}
*/
public DiffTree setRecursive(boolean recursive) {
this.recursive = recursive;
return this;
}
}