org.locationtech.geogig.plumbing.merge.DiffMergeFeaturesOp.java Source code

Java tutorial

Introduction

Here is the source code for org.locationtech.geogig.plumbing.merge.DiffMergeFeaturesOp.java

Source

/* Copyright (c) 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.plumbing.merge;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.eclipse.jdt.annotation.Nullable;
import org.locationtech.geogig.model.ObjectId;
import org.locationtech.geogig.model.RevFeature;
import org.locationtech.geogig.model.RevFeatureType;
import org.locationtech.geogig.model.RevObject;
import org.locationtech.geogig.model.impl.RevFeatureBuilder;
import org.locationtech.geogig.plumbing.DiffFeature;
import org.locationtech.geogig.plumbing.diff.AttributeDiff;
import org.locationtech.geogig.plumbing.diff.FeatureDiff;
import org.locationtech.geogig.plumbing.merge.DiffMergeFeaturesOp.DiffMergeFeatureResult;
import org.locationtech.geogig.repository.AbstractGeoGigOp;
import org.locationtech.geogig.repository.NodeRef;
import org.locationtech.geogig.storage.BulkOpListener;
import org.locationtech.geogig.storage.ObjectDatabase;
import org.opengis.feature.type.GeometryDescriptor;
import org.opengis.feature.type.PropertyDescriptor;

import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.vividsolutions.jts.geom.Geometry;

/**
 * Takes a feature reference to a common ancestor and the two ends of a feature merge and produces a
 * {@link DiffMergeFeatureResult result} that indicates whether the features conflict, or can be
 * merged, and in that case can create the merged feature.
 * <p>
 * 
 * @implNote this operation is like combining two {@link DiffFeature} and one
 *           {@link MergeFeaturesOp} commands into one, with the benefit of less calls the
 *           {@link ObjectDatabase}, and was created to improve the performance of
 *           {@link ReportMergeScenarioOp} when dealing with features that changed at both ends of
 *           the merge.
 * 
 * @see ReportMergeScenarioOp
 */
public class DiffMergeFeaturesOp extends AbstractGeoGigOp<DiffMergeFeatureResult> {

    /**
     * The result of creating the {@link FeatureDiff diff} between the "ours" and "theirs" versions
     * of a feature while merging a branch.
     *
     */
    public static class DiffMergeFeatureResult {

        private FeatureDiff mergeIntoDiff;

        private FeatureDiff toMergeDiff;

        public DiffMergeFeatureResult(FeatureDiff mergeIntoDiff, FeatureDiff toMergeDiff) {
            this.mergeIntoDiff = mergeIntoDiff;
            this.toMergeDiff = toMergeDiff;
        }

        /**
         * @return {@code true} if the result is a merged feature
         */
        public boolean isMerge() {
            return !toMergeDiff.equals(mergeIntoDiff);
        }

        /**
         * Returns {@code true} if there's a conflict in merging the two features.
         * <p>
         * A conflict can happen if the feature types are incompatible or both changed the same
         * feature attribute with different values.
         * 
         * @return {@code true} if the features can't be merged due to a conflict.
         */
        public boolean isConflict() {
            if (mergeIntoDiff.conflicts(toMergeDiff)) {
                return true;
            }
            if (!featureTypesMatch()) {
                return true;
            }
            return false;
        }

        private boolean featureTypesMatch() {
            // if the feature types are different we report a conflict and do not
            // try to perform automerge
            RevFeatureType oursft = mergeIntoDiff.getNewFeatureType();
            RevFeatureType theirsft = toMergeDiff.getNewFeatureType();
            return oursft.equals(theirsft);
        }

        /**
         * @return the merged feature.
         * @precondition the feature types are compatible.
         */
        public RevFeature mergedFeature() {
            checkState(featureTypesMatch());
            return merge(mergeIntoDiff, toMergeDiff);
        }

    }

    private NodeRef commonAncestor;

    private NodeRef mergeInto;

    private NodeRef toMerge;

    public DiffMergeFeaturesOp setCommonAncestor(NodeRef commonAncestor) {
        this.commonAncestor = commonAncestor;
        return this;
    }

    /**
     * @param ours the ref that points to the "old" version of the feature to compare
     * @return {@code this}
     */
    public DiffMergeFeaturesOp setMergeInto(NodeRef ours) {
        this.mergeInto = ours;
        return this;
    }

    /**
     * @param mergeIntoNodeRef the ref that points to the "old" version of the feature to compare
     * @return {@code this}
     */
    public DiffMergeFeaturesOp setToMerge(NodeRef theirs) {
        this.toMerge = theirs;
        return this;
    }

    @Override
    protected DiffMergeFeatureResult _call() {
        checkPreconditions(commonAncestor, mergeInto, toMerge);

        final Map<ObjectId, RevObject> objects = getObjects();

        RevFeature ancestorF = (RevFeature) objects.get(commonAncestor.getObjectId());
        RevFeature mergeIntoF = (RevFeature) objects.get(mergeInto.getObjectId());
        RevFeature toMergeF = (RevFeature) objects.get(toMerge.getObjectId());

        RevFeatureType ancestorT = (RevFeatureType) objects.get(commonAncestor.getMetadataId());
        RevFeatureType mergetIntoT = (RevFeatureType) objects.get(mergeInto.getMetadataId());
        RevFeatureType toMergeT = (RevFeatureType) objects.get(toMerge.getMetadataId());

        FeatureDiff mergeIntoDiff = compare(ancestorF, mergeIntoF, ancestorT, mergetIntoT);
        FeatureDiff toMergeDiff = compare(ancestorF, toMergeF, ancestorT, toMergeT);

        return new DiffMergeFeatureResult(mergeIntoDiff, toMergeDiff);
    }

    private Map<ObjectId, RevObject> getObjects() {

        final ObjectId ancestorMetadataId = commonAncestor.getMetadataId();
        final ObjectId mergetIntoMetadataId = mergeInto.getMetadataId();
        final ObjectId toMergeMetadataId = toMerge.getMetadataId();

        final ObjectId ancestorFeatureId = commonAncestor.getObjectId();
        final ObjectId featureAId = mergeInto.getObjectId();
        final ObjectId featureBId = toMerge.getObjectId();

        Set<ObjectId> ids = Sets.newHashSet(ancestorMetadataId, mergetIntoMetadataId, toMergeMetadataId,
                ancestorFeatureId, featureAId, featureBId);

        Iterator<RevObject> objsit = objectDatabase().getAll(ids, BulkOpListener.NOOP_LISTENER);

        ImmutableMap<ObjectId, RevObject> map = Maps.uniqueIndex(objsit, (o) -> o.getId());

        if (ids.size() != map.size()) {
            ids.forEach((id) -> checkState(map.containsKey(id), "Invalid reference: %s", id));
        }
        return map;
    }

    private void checkPreconditions(NodeRef commonAncestor, NodeRef mergeInto, NodeRef toMerge) {

        checkNotNull(commonAncestor, "common ancestor version not specified");
        checkNotNull(mergeInto, "old version not specified");
        checkNotNull(toMerge, "new version not specified");

        checkArgument(
                commonAncestor.path().equals(mergeInto.path()) && commonAncestor.path().equals(toMerge.path()));
    }

    static RevFeature merge(FeatureDiff mergeIntoDiff, FeatureDiff toMergeDiff) {

        if (!mergeIntoDiff.getNewFeatureType().equals(toMergeDiff.getNewFeatureType())) {
            throw new IllegalArgumentException(
                    String.format("Non-matching feature types. Cannot merge. Left: %s, right: %s",
                            mergeIntoDiff.getNewFeatureType().getId(), toMergeDiff.getNewFeatureType().getId()));
        }

        RevFeatureType featureType = mergeIntoDiff.getNewFeatureType();

        Map<PropertyDescriptor, AttributeDiff> leftDiffs = mergeIntoDiff.getDiffs();
        Map<PropertyDescriptor, AttributeDiff> rightDiffs = toMergeDiff.getDiffs();

        ImmutableList<PropertyDescriptor> descriptors;
        descriptors = ImmutableList.copyOf(featureType.type().getDescriptors());

        final List<Object> ancestorValues;
        ancestorValues = getAncestorValues(mergeIntoDiff, toMergeDiff, descriptors);

        RevFeatureBuilder mergedValues = RevFeatureBuilder.builder();

        for (int i = 0; i < descriptors.size(); i++) {
            final PropertyDescriptor descriptor = descriptors.get(i);
            final boolean isGeom = descriptor instanceof GeometryDescriptor;

            @Nullable
            Object ancestorValue = ancestorValues.get(i);
            @Nullable
            AttributeDiff leftAttDiff = leftDiffs.get(descriptor);
            @Nullable
            AttributeDiff rightAttDiff = rightDiffs.get(descriptor);

            Object valueA = leftAttDiff == null ? null : leftAttDiff.getNewValue();
            Object valueB = rightAttDiff == null ? null : rightAttDiff.getNewValue();

            Object merged;

            if (leftAttDiff == null) {
                merged = rightAttDiff == null ? ancestorValue : valueB;
            } else if (rightAttDiff == null) {
                merged = valueA;
            } else if (valueEquals(isGeom, valueA, valueB)) {
                merged = valueA;
            } else {
                // both modified the attribute and didn't set the same value
                merged = valueA;
            }
            mergedValues.addValue(merged);
        }

        return mergedValues.build();
    }

    private static List<Object> getAncestorValues(FeatureDiff mergeIntoDiff, FeatureDiff toMergeDiff,
            ImmutableList<PropertyDescriptor> descriptors) {
        final List<Object> ancestorValues;
        {
            RevFeature ancestor = mergeIntoDiff.getOldFeature() == null ? toMergeDiff.getOldFeature()
                    : mergeIntoDiff.getOldFeature();
            if (ancestor == null) {
                Object[] array = new Optional[descriptors.size()];
                ancestorValues = Arrays.asList(array);
            } else {
                ancestorValues = new ArrayList<>(ancestor.size());
                ancestor.forEach((v) -> ancestorValues.add(v));
            }
        }
        return ancestorValues;
    }

    private static boolean valueEquals(boolean isGeom, @Nullable Object v1, @Nullable Object v2) {
        return isGeom ? geomEquals((Geometry) v1, (Geometry) v2) : Objects.equals(v1, v2);
    }

    private static boolean geomEquals(@Nullable Geometry g1, @Nullable Geometry g2) {
        if (g1 == null || g2 == null) {
            return g1 == null && g2 == null;
        }
        return g1.equalsExact(g2);
    }

    private FeatureDiff compare(RevFeature oldRevFeature, RevFeature newRevFeature,
            RevFeatureType oldRevFeatureType, RevFeatureType newRevFeatureType) {

        return new FeatureDiff(mergeInto.path(), newRevFeature, oldRevFeature, newRevFeatureType, oldRevFeatureType,
                false);
    }

}