com.redhat.lightblue.mongo.crud.BsonMerge.java Source code

Java tutorial

Introduction

Here is the source code for com.redhat.lightblue.mongo.crud.BsonMerge.java

Source

/*
 Copyright 2013 Red Hat, Inc. and/or its affiliates.
    
 This file is part of lightblue.
    
 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.
    
 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.
    
 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.redhat.lightblue.mongo.crud;

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.mongodb.DBObject;
import com.mongodb.BasicDBObject;

import com.redhat.lightblue.metadata.EntityMetadata;

import com.redhat.lightblue.util.DocComparator;
import com.redhat.lightblue.util.Path;
import com.redhat.lightblue.util.MutablePath;

/**
 * During a save operation, the document provided by the client replaces the
 * copy in the database. If the client has a more limited view of data than is
 * already present (i.e. client using an earlier version of metadata), then
 * there may be some invisible fields, fields that are in the document, but not
 * in the metadata used by the client. To prevent overwriting those fields, we
 * perform a merge operation: all invisible fields are preserved in the updated
 * document.
 */
public final class BsonMerge extends DocComparator<Object, Object, DBObject, List> {

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

    private final EntityMetadata md;
    private final HashSet<Path> ignoredIdentities = new HashSet<>();

    public BsonMerge(EntityMetadata md) {
        this.md = md;
        Map<Path, List<Path>> idMap = md.getEntitySchema().getArrayIdentities();
        for (Map.Entry<Path, List<Path>> entry : idMap.entrySet()) {
            addArrayIdentity(entry.getKey(), entry.getValue().toArray(new Path[entry.getValue().size()]));
        }
    }

    public class BsonIdentityExtractor implements IdentityExtractor<Object> {
        private final Path[] fields;

        public BsonIdentityExtractor(ArrayIdentityFields fields) {
            this.fields = fields.getFields();
        }

        @Override
        public Object getIdentity(Object element) {
            Object[] nodes = new Object[fields.length];
            for (int i = 0; i < fields.length; i++) {
                nodes[i] = DocTranslator.getDBObject((DBObject) element, fields[i]);
            }
            return new DefaultIdentity(nodes);
        }
    }

    @Override
    protected boolean isValue(Object value) {
        return !(value instanceof Collection) && !(value instanceof DBObject);
    }

    @Override
    protected boolean isArray(Object value) {
        return value instanceof Collection;
    }

    @Override
    protected boolean isObject(Object value) {
        return value instanceof DBObject;
    }

    @Override
    protected boolean isNull(Object value) {
        return value == null;
    }

    @Override
    protected Object asValue(Object value) {
        return value;
    }

    @Override
    protected List asArray(Object value) {
        return (List) value;
    }

    @Override
    protected DBObject asObject(Object value) {
        return (DBObject) value;
    }

    @Override
    protected boolean equals(Object value1, Object value2) {
        return (value1 == null && value2 == null) || (value1 != null && value1.equals(value2));
    }

    @Override
    protected Iterator<Map.Entry<String, Object>> getFields(DBObject node) {
        return node.toMap().entrySet().iterator();
    }

    @Override
    protected boolean hasField(DBObject value, String field) {
        return value.containsField(field);
    }

    @Override
    protected Object getField(DBObject value, String field) {
        return value.get(field);
    }

    @Override
    protected IdentityExtractor getArrayIdentityExtractorImpl(ArrayIdentityFields fields) {
        return new BsonIdentityExtractor(fields);
    }

    @Override
    protected Object getElement(List value, int index) {
        return value.get(index);
    }

    @Override
    protected int size(List value) {
        return value.size();
    }

    @Override
    public IdentityExtractor getArrayIdentityExtractor(Path arrayField) {
        MutablePath p = new MutablePath();
        int n = arrayField.numSegments();
        for (int i = 0; i < n; i++) {
            if (arrayField.isIndex(i)) {
                p.push(Path.ANY);
            } else {
                p.push(arrayField.head(i));
            }
        }
        if (ignoredIdentities.contains(p))
            return null;
        else {
            ArrayIdentityFields fields = getArrayIdentities().get(p);
            if (fields != null) {
                return getArrayIdentityExtractorImpl(fields);
            }
        }
        return null;
    }

    /**
    * Input is a path to an array element
    * Output is the array containing that element, and all indexes replaced with *
    */
    private Path getArray(Path p) {
        int lastIndex = p.numSegments() - 1;
        while (lastIndex > 0) {
            if (p.isIndex(lastIndex))
                break;
            lastIndex--;
        }
        MutablePath mp = new MutablePath();
        for (int i = 0; i < lastIndex; i++) {
            if (p.isIndex(i))
                mp.push(Path.ANY);
            else
                mp.push(p.head(i));
        }
        return mp.immutableCopy();
    }

    public DocComparator.Difference<Object> compareNodesWithFallback(DBObject oldDoc, DBObject newDoc)
            throws Exception {
        // Compare two docs. If we get a duplicate array identity exception, remove that array identity from
        // the identity map, and compare again. This is to deal with docs that contain duplicate array identities either
        // because the identity constraint is added after data is screwed up, or becase we forgot to validate it

        int n = getArrayIdentities().size() + 1;
        // This can loop at most the number of array identities times + 1 (if there are none, it should do it once)
        for (int i = 0; i < n; i++) {
            try {
                return compareNodes(oldDoc, newDoc);
            } catch (DocComparator.DuplicateArrayIdentity dai) {
                LOGGER.warn("Duplicate array element id for doc._id:{}, field:{}", oldDoc.get("_id"),
                        dai.getPath());
                Path p = dai.getPath();
                ignoredIdentities.add(getArray(p));
                LOGGER.debug("Ignored paths:{}", ignoredIdentities);
            } catch (DocComparator.InvalidArrayIdentity iai) {
                LOGGER.warn("Invalid array element id for doc._id:{}, field:{}", oldDoc.get("_id"), iai.getPath());
                Path p = iai.getPath();
                ignoredIdentities.add(getArray(p));
                LOGGER.debug("Ignored paths:{}", ignoredIdentities);
            }
        }
        throw new RuntimeException(
                "Cannot compute document diffs even with ignored identities:" + ignoredIdentities);
    }

    /**
     * Copies all fields in oldDoc that are not in metadata into newDoc
     *
     * @return true if some fields are copied
     */
    public boolean merge(DBObject oldDoc, DBObject newDoc) {
        boolean ret = false;
        try {
            LOGGER.debug("Merge start");
            DocComparator.Difference<Object> diff = compareNodesWithFallback(oldDoc, newDoc);
            LOGGER.debug("Diff:{}" + diff);

            // We look for things removed in the new document
            for (DocComparator.Delta<Object> delta : diff.getDelta()) {
                if (delta instanceof DocComparator.Removal) {
                    DocComparator.Removal<Object> removal = (DocComparator.Removal<Object>) delta;
                    Path removedField = removal.getField1();

                    boolean hidden = true;

                    // If this is a removed array element, we don't add it back
                    int numSegments = removedField.numSegments();
                    if (numSegments > 1 && removedField.isIndex(numSegments - 1)) {
                        hidden = false;
                    }
                    if (hidden) {
                        // Is the removed field in metadata?
                        try {
                            // don't try to resolve a hidden field
                            if (!hasHiddenField(removedField)) {
                                md.resolve(removedField);
                                hidden = false;
                            } else {
                                hidden = true;
                            }
                        } catch (Exception e) {
                            hidden = true;
                        }
                        if (hidden) {
                            LOGGER.debug("Field {} is hidden but removed, adding it back", removedField);
                            addField(newDoc, diff.getDelta(), removal);
                            ret = true;
                        }
                    }
                }
            }
        } catch (Exception e) {
            LOGGER.error("Error in merge:{}", e, e);
            throw new RuntimeException(e);
        }
        return ret;
    }

    private boolean hasHiddenField(Path path) {
        for (int i = 0; i < path.numSegments(); i++) {
            if (path.head(i).equals(DocTranslator.HIDDEN_SUB_PATH.toString())) {
                return true;
            }
        }
        return false;
    }

    private void addField(DBObject doc, List<DocComparator.Delta<Object>> delta,
            DocComparator.Removal<Object> removedField) {
        Path field = removedField.getField1();
        // 'field' is the name of the removed field in the old document.
        // We have to find the field name it has in the new document, because array elements might have moved around

        MutablePath newFieldName = new MutablePath();
        int n = field.numSegments();
        for (int i = 0; i < n; i++) {
            if (field.isIndex(i)) {
                // At this point, newFieldName points to an array,
                // field.prefix(i) points to an array element. If there
                // is a Move delta for the move of field.prefix(i+1)
                // -> someField, then we get the new index from
                // someField
                Path arrayElement = field.prefix(i + 1);
                // arrayElement.tail(0) is an index
                Path movedTo = null;
                for (DocComparator.Delta<Object> d : delta) {
                    if (d instanceof DocComparator.Move) {
                        DocComparator.Move<Object> move = (DocComparator.Move<Object>) d;
                        if (move.getField1().equals(arrayElement)) {
                            movedTo = move.getField2();
                            break;
                        }
                    }
                }
                LOGGER.debug("field: {}, arrayElement: {}, movedTo:{}", field, arrayElement, movedTo);
                if (movedTo != null) {
                    // arrayElement moved to somewhere else, get the new index, push it to the newFieldName
                    newFieldName.push(movedTo.tail(0));
                } else {
                    // ArrayElement did not move
                    newFieldName.push(field.head(i));
                }
            } else {
                newFieldName.push(field.head(i));
            }
        }
        // At this point, newFieldName contains the field in the new document
        LOGGER.debug("Adding contents of {} to {}", field, newFieldName);
        DBObject parent;
        if (newFieldName.numSegments() > 1) {
            parent = (DBObject) DocTranslator.getDBObject(doc, newFieldName.prefix(-1));
        } else {
            parent = doc;
        }
        parent.put(newFieldName.tail(0), copy(removedField.getRemovedNode()));
    }

    private static Object copy(Object fld) {
        Object ret;
        if (fld instanceof List) {
            ArrayList<Object> l = new ArrayList<>();
            for (Object s : (List) fld) {
                l.add(copy(s));
            }
            ret = l;
        } else if (fld instanceof DBObject) {
            DBObject r = new BasicDBObject();
            for (String s : ((DBObject) fld).keySet()) {
                r.put(s, copy(((DBObject) fld).get(s)));
            }
            ret = r;
        } else {
            ret = fld;
        }
        return ret;
    }
}