Package org.apache.jackrabbit.mongomk.impl.command

Source Code of org.apache.jackrabbit.mongomk.impl.command.MergeCommand

/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements.  See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License.  You may obtain a copy of the License at
*
*      http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.jackrabbit.mongomk.impl.command;

import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.jackrabbit.mk.model.tree.DiffBuilder;
import org.apache.jackrabbit.mk.model.tree.NodeState;
import org.apache.jackrabbit.mongomk.api.command.Command;
import org.apache.jackrabbit.mongomk.api.model.Commit;
import org.apache.jackrabbit.mongomk.api.model.Node;
import org.apache.jackrabbit.mongomk.impl.MongoNodeStore;
import org.apache.jackrabbit.mongomk.impl.action.FetchCommitAction;
import org.apache.jackrabbit.mongomk.impl.action.FetchCommitsAction;
import org.apache.jackrabbit.mongomk.impl.action.FetchHeadRevisionIdAction;
import org.apache.jackrabbit.mongomk.impl.json.JsopParser;
import org.apache.jackrabbit.mongomk.impl.json.NormalizingJsopHandler;
import org.apache.jackrabbit.mongomk.impl.model.CommitBuilder;
import org.apache.jackrabbit.mongomk.impl.model.MongoCommit;
import org.apache.jackrabbit.mongomk.impl.model.NodeImpl;
import org.apache.jackrabbit.mongomk.impl.model.tree.MongoNodeDelta;
import org.apache.jackrabbit.mongomk.impl.model.tree.MongoNodeDelta.Conflict;
import org.apache.jackrabbit.mongomk.impl.model.tree.MongoNodeState;
import org.apache.jackrabbit.mongomk.impl.model.tree.SimpleMongoNodeStore;
import org.apache.jackrabbit.mongomk.util.MongoUtil;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* A {@code Command} for {@code MongoMicroKernel#merge(String, String)}
*/
public class MergeCommand extends BaseCommand<String> {

    private static final Logger LOG = LoggerFactory.getLogger(MergeCommand.class);

    private final String branchRevisionId;
    private final String message;

    /**
     * Constructs a {@code MergeCommandMongo}
     *
     * @param nodeStore Node store.
     * @param branchRevisionId Branch revision id.
     * @param message Merge message.
     */
    public MergeCommand(MongoNodeStore nodeStore, String branchRevisionId,
            String message) {
        super(nodeStore);
        this.branchRevisionId = branchRevisionId;
        this.message = message;
    }

    @Override
    public String execute() throws Exception {
        MongoCommit commit = new FetchCommitAction(nodeStore,
                MongoUtil.toMongoRepresentation(branchRevisionId)).execute();
        String branchId = commit.getBranchId();
        if (branchId == null) {
            throw new Exception("Can only merge a private branch commit");
        }

        long rootNodeId = commit.getRevisionId();

        FetchHeadRevisionIdAction query2 = new FetchHeadRevisionIdAction(nodeStore);
        long currentHead = query2.execute();

        long branchRootId = Long.parseLong(branchId.substring(0, branchId.indexOf("-")));

        Commit newCommit;
        String diff = getNonConflictingCommitsDiff(Math.max(currentHead, commit.getRevisionId()),
                branchRootId, branchId);
        if (diff != null) {
            newCommit = CommitBuilder.build("/", diff.toString(),
                    MongoUtil.fromMongoRepresentation(currentHead), message);
        } else {
            Node ourRoot = getNode("/", rootNodeId, branchId);

            // Merge changes, if any, from trunk to branch.
            Node currentHeadNode = getNode("/", currentHead);
            if (currentHead != branchRootId) {
                ourRoot = mergeNodes(ourRoot, currentHeadNode, branchRootId);
            }

            diff = new DiffBuilder(MongoUtil.wrap(currentHeadNode),
                    MongoUtil.wrap(ourRoot), "/", -1,
                    new SimpleMongoNodeStore(), "").build();

            if (diff.isEmpty()) {
                LOG.debug("Merge of empty branch {} with differing content hashes encountered, " +
                        "ignore and keep current head {}", branchRevisionId, currentHead);
                return MongoUtil.fromMongoRepresentation(currentHead);
            }

            newCommit = CommitBuilder.build("", diff,
                    MongoUtil.fromMongoRepresentation(currentHead), message);
        }

        Command<Long> command = new CommitCommandNew(nodeStore, newCommit);
        long revision = command.execute();
        return MongoUtil.fromMongoRepresentation(revision);
    }

    /**
     * Checks whether there have been no conflicting commits on trunk since the
     * branch was created and if so, returns the branch diff to be applied against
     * trunk. Otherwise, it returns null.
     *
     * @param currentHead
     * @param branchRootId
     * @param branchId
     * @return
     */
    private String getNonConflictingCommitsDiff(long currentHead, long branchRootId, String branchId) {
        FetchCommitsAction action = new FetchCommitsAction(nodeStore, branchRootId + 1, currentHead);
        List<MongoCommit> commits = action.execute();

        Set<String> affectedPathsBranch = new HashSet<String>();
        Set<String> affectedPathsTrunk = new HashSet<String>();
        StringBuilder diff = new StringBuilder();

        for (int i = commits.size() - 1; i >= 0; i--) {
            MongoCommit commit = commits.get(i);
            Set<String> affectedPaths = commit.getAffectedPaths();
            for (String affectedPath : affectedPaths) {
                if (branchId.equals(commit.getBranchId())) {
                    if (affectedPathsTrunk.contains(affectedPath)) {
                        return null;
                    }
                    affectedPathsBranch.add(affectedPath);
                } else if (commit.getBranchId() == null){
                    if (affectedPathsBranch.contains(affectedPath)) {
                        return null;
                    }
                    affectedPathsTrunk.add(affectedPath);
                }
            }
            if (branchId.equals(commit.getBranchId())) {
                try {
                    diff.append(normalizeDiff(commit.getPath(), commit.getDiff()));
                } catch (Exception e) {
                    LOG.error("Normalization error", e);
                }
            }
        }
        return diff.length() > 0 ? diff.toString() : null;
    }

    /**
     * Normalizes a JSOP diff by appending the path to all pathStrings of the
     * operations.
     *
     * @param path the root path of the diff.
     * @param diff the JSOP diff.
     * @return the JSOP diff based on an empty root path.
     */
    private String normalizeDiff(String path, String diff) throws Exception {
        NormalizingJsopHandler handler = new NormalizingJsopHandler();
        new JsopParser(path, diff, handler).parse();
        return handler.getDiff();
    }

    private NodeImpl mergeNodes(Node ourRoot, Node theirRoot,
            Long commonAncestorRevisionId) throws Exception {

        Node baseRoot = getNode("/", commonAncestorRevisionId);
        Node theirRootCopy = copy(theirRoot);

        // Recursively merge 'our' changes with 'their' changes...
        return mergeNode(baseRoot, ourRoot, theirRootCopy, "/");
    }

    private NodeImpl mergeNode(Node baseNode, Node ourNode, Node theirNode,
            String path) throws Exception {
        MongoNodeDelta theirChanges = new MongoNodeDelta(new SimpleMongoNodeStore(),
                MongoUtil.wrap(baseNode), MongoUtil.wrap(theirNode));
        MongoNodeDelta ourChanges = new MongoNodeDelta(new SimpleMongoNodeStore(),
                MongoUtil.wrap(baseNode), MongoUtil.wrap(ourNode));

        NodeImpl stagedNode = (NodeImpl)theirNode; //new NodeImpl(path);

        // Apply our changes.
        stagedNode.getProperties().putAll(ourChanges.getAddedProperties());
        stagedNode.getProperties().putAll(ourChanges.getChangedProperties());
        for (String name : ourChanges.getRemovedProperties().keySet()) {
            stagedNode.getProperties().remove(name);
        }

        for (Map.Entry<String, NodeState> entry : ourChanges.getAddedChildNodes().entrySet()) {
            MongoNodeState nodeState = (MongoNodeState)entry.getValue();
            stagedNode.addChildNodeEntry(nodeState.unwrap());
        }
        for (Map.Entry<String, NodeState> entry : ourChanges.getChangedChildNodes().entrySet()) {
            if (!theirChanges.getChangedChildNodes().containsKey(entry.getKey())) {
                MongoNodeState nodeState = (MongoNodeState)entry.getValue();
                stagedNode.addChildNodeEntry(nodeState.unwrap());
            }
        }
        for (String name : ourChanges.getRemovedChildNodes().keySet()) {
            stagedNode.removeChildNodeEntry(name);
        }

        List<Conflict> conflicts = theirChanges.listConflicts(ourChanges);
        // resolve/report merge conflicts
        for (Conflict conflict : conflicts) {
            String conflictName = conflict.getName();
            String conflictPath = PathUtils.concat(path, conflictName);
            switch (conflict.getType()) {
                case PROPERTY_VALUE_CONFLICT:
                    throw new Exception(
                            "concurrent modification of property " + conflictPath
                                    + " with conflicting values: \""
                                    + ourNode.getProperties().get(conflictName)
                                    + "\", \""
                                    + theirNode.getProperties().get(conflictName));

                case NODE_CONTENT_CONFLICT: {
                    if (ourChanges.getChangedChildNodes().containsKey(conflictName)) {
                        // modified subtrees
                        Node baseChild = baseNode.getChildNodeEntry(conflictName);
                        Node ourChild = ourNode.getChildNodeEntry(conflictName);
                        Node theirChild = theirNode.getChildNodeEntry(conflictName);
                        // merge the dirty subtrees recursively
                        mergeNode(baseChild, ourChild, theirChild, PathUtils.concat(path, conflictName));
                    } else {
                        // todo handle/merge colliding node creation
                        throw new Exception("colliding concurrent node creation: " + conflictPath);
                    }
                    break;
                }

                case REMOVED_DIRTY_PROPERTY_CONFLICT:
                    stagedNode.getProperties().remove(conflictName);
                    break;

                case REMOVED_DIRTY_NODE_CONFLICT:
                    //stagedNode.remove(conflictName);
                    stagedNode.removeChildNodeEntry(conflictName);
                    break;
            }

        }
        return stagedNode;
    }

    private Node getNode(String path, long revisionId) throws Exception {
        return getNode(path, revisionId, null);
    }

    private Node getNode(String path, long revisionId, String branchId) throws Exception {
        GetNodesCommandNew command = new GetNodesCommandNew(nodeStore, path, revisionId);
        command.setBranchId(branchId);
        return command.execute();
    }

    private NodeImpl copy(Node node) {
        NodeImpl copy = new NodeImpl(node.getPath());
        copy.setRevisionId(node.getRevisionId());
        for (Map.Entry<String, String> entry : node.getProperties().entrySet()) {
            copy.addProperty(entry.getKey(), entry.getValue());
        }
        for (Iterator<Node> it = node.getChildNodeEntries(0, -1); it.hasNext(); ) {
            Node child = it.next();
            copy.addChildNodeEntry(copy(child));
        }
        return copy;
    }

}
TOP

Related Classes of org.apache.jackrabbit.mongomk.impl.command.MergeCommand

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.