All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.redhat.lightblue.crud.mongo.Merge Maven / Gradle / Ivy

The newest version!
/*
 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 .
 */
package com.redhat.lightblue.crud.mongo;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.Map;
import java.util.HashMap;

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

import com.mongodb.DBObject;
import com.redhat.lightblue.metadata.FieldCursor;
import com.redhat.lightblue.metadata.FieldTreeNode;
import com.redhat.lightblue.metadata.SimpleField;
import com.redhat.lightblue.metadata.ObjectField;
import com.redhat.lightblue.metadata.EntityMetadata;
import com.redhat.lightblue.metadata.FieldConstraint;
import com.redhat.lightblue.metadata.constraints.ArrayElementIdConstraint;
import com.redhat.lightblue.metadata.types.UIDType;
import com.redhat.lightblue.util.Error;
import com.redhat.lightblue.util.MutablePath;
import com.redhat.lightblue.util.Path;

/**
 * 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 Merge {

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

    private final EntityMetadata md;

    public static final class IField {
        private final Path path;
        private final Object value;

        public IField(Path p, Object value) {
            path = p;
            this.value = value;
        }

        public Path getPath() {
            return path;
        }

        public Object getValue() {
            return value;
        }

        @Override
        public String toString() {
            return path.toString() + ":" + value;
        }
    }

    public static final class PathAndField {
        private final Path path;
        private final SimpleField field;

        public PathAndField(Path path, SimpleField field) {
            this.path = path;
            this.field = field;
        }

        public Path getPath() {
            return path;
        }

        public SimpleField getField() {
            return field;
        }

        @Override
        public String toString() {
            return path.toString();
        }
    }

    private final List invisibleFields = new ArrayList<>();
    private final Map> arrayIdentifiers = new HashMap<>();

    /**
     * Initialize with the metadata of the new object
     */
    public Merge(EntityMetadata md) {
        this.md = md;
    }

    /**
     * Reset the internal state of the Merge
     */
    public void reset() {
        invisibleFields.clear();
        arrayIdentifiers.clear();
    }

    /**
     * Attempts to copy the invisible fields in oldCopy into newCopy. If the
     * attemp is unsucessful, Error is thrown.
     */
    public void merge(DBObject oldCopy, DBObject newCopy) {
        reset();
        findInvisibleFields(oldCopy);
        if (!invisibleFields.isEmpty()) {
            mergeIn(oldCopy, newCopy);
        }
    }

    public List getInvisibleFields() {
        return (List) ((ArrayList) invisibleFields).clone();
    }

    private void mergeIn(DBObject oldCopy, DBObject newCopy) {
        // Process invisible fields one by one
        for (IField ifield : invisibleFields) {
            Path p = ifield.getPath();
            DBObject parent = findMergeParent(oldCopy, newCopy, p);
            if (parent == null) {
                throw Error.get(MongoCrudConstants.ERR_SAVE_CLOBBERS_HIDDEN_FIELDS, p.toString());
            }
            ((DBObject) parent).put(p.tail(0), ifield.getValue());
        }
    }

    /**
     * Tries to locate the parent DBObject object that will be the parent of the
     * field that needs to be added to tbe newCopy to preverse the field
     */
    private DBObject findMergeParent(DBObject oldCopy, DBObject newCopy, Path field) {
        LOGGER.debug("Attempting to merge in {}", field);
        // Descend all the way to the parent of the field. Descend on both the oldCopy and the newCopy
        int n = field.numSegments();
        DBObject ret = null;
        if (n > 1) {
            int parentLevel = n - 1;
            Object parent = newCopy;
            Object oldParent = oldCopy;
            boolean fail = false;
            for (int segment = 0; segment < parentLevel; segment++) {
                if (field.isIndex(segment)) {
                    // This is an array
                    // See if we have any identifiers for this array

                    Path elemField = field.prefix(segment+1);
                    Path arrayField = elemField.prefix(segment);
                    // elemField points to arrayElement (e.g. x.y.z.1)
                    // arrayField points to array (e.g. x.y.z)
                    List identifiers = arrayIdentifiers.get(arrayField);
                    if (identifiers == null) {
                        identifiers = getArrayIdentifiers(elemField);
                        if (!identifiers.isEmpty()) {
                            arrayIdentifiers.put(arrayField, identifiers);
                        }
                    }
                    LOGGER.debug("Identifiers for array field {}: {}", field, identifiers);
                    if (identifiers.isEmpty()) {
                        fail = true;
                        break;
                    } else {
                        // Retrieve identifying content
                        List identifyingContent = getIdentifyingContent(identifiers,
                                ((List) oldParent).
                                get(field.getIndex(segment)));
                        LOGGER.debug("Identifying content: {}", identifyingContent);
                        // Find the array element in the new copy
                        DBObject newArrayElement = findArrayElement((List) parent, identifyingContent);
                        if (newArrayElement != null) {
                            // Found new array element.
                            LOGGER.debug("Found element in newCopy: {} ", newArrayElement);
                            oldParent = ((List) oldParent).get(field.getIndex(segment));
                            parent = newArrayElement;
                        } else {
                            LOGGER.debug("Not found element in newCopy with identifiers {}", identifyingContent);
                        }
                    }
                } else {
                    if (parent instanceof DBObject) {
                        String seg = field.head(segment);
                        // This is a nested object
                        Object x = ((DBObject) parent).get(seg);
                        // If the nested object is removed in the new copy,
                        // merging results in data loss, so we fail
                        if (x == null) {
                            // Fail: cannot merge
                            fail = true;
                            break;
                        }
                        parent = x;
                        // The following is guaranteed to succeed. We
                        // built the path from oldCopy.
                        oldParent = ((DBObject) oldParent).get(seg);
                    } else {
                        // Fail: cannot merge
                        fail = true;
                        break;
                    }
                }
            }
            if (!fail) {
                ret = (DBObject) parent;
            }
        } else {
            ret = newCopy;
        }
        return ret;
    }

    public List getIdentifyingContent(List identifiers, DBObject parent) {
        List values = new ArrayList<>(identifiers.size());
        for (PathAndField pf : identifiers) {
            Object value = getValue(parent, pf.getPath());
            values.add(new IField(pf.getPath(), value));
        }
        return values;
    }

    private Object getValue(Object parent, Path relpath) {
        int n = relpath.numSegments();
        for (int i = 0; i < n; i++) {
            parent = ((DBObject) parent).get(relpath.head(i));
            if (parent == null) {
                break;
            }
        }
        return parent;
    }

    /**
     * Returns all array element ids. If there are none, returns all UID fields.
     * Search starts from the array element, and does not descend into any
     * nested arrays.
     */
    public List getArrayIdentifiers(Path arrayElementField) {
        List idPaths = new ArrayList<>();
        MutablePath mp = new MutablePath();
        getArrayIdentifiers(md.getFieldCursor(arrayElementField), mp, idPaths, new ArrayIdCollector() {
            @Override
            public boolean isIncluded(SimpleField field) {
                List constraints = field.getConstraints();
                if (constraints != null) {
                    for (FieldConstraint x : constraints) {
                        if (x instanceof ArrayElementIdConstraint) {
                            return true;
                        }
                    }
                }
                return false;
            }
        });
        if (idPaths.isEmpty()) {
            getArrayIdentifiers(md.getFieldCursor(arrayElementField), mp, idPaths, new ArrayIdCollector() {
                @Override
                public boolean isIncluded(SimpleField field) {
                    return field.getType().equals(UIDType.TYPE);
                }
            });
        }
        return idPaths;
    }

    private interface ArrayIdCollector {
        boolean isIncluded(SimpleField field);
    }

    private void getArrayIdentifiers(FieldCursor cursor, MutablePath mp,
                                     List paths, ArrayIdCollector collector) {
        if (cursor.firstChild()) {
            mp.push("x");
            do {
                FieldTreeNode fn = cursor.getCurrentNode();
                mp.setLast(fn.getName());
                if (fn instanceof ObjectField) {
                    getArrayIdentifiers(cursor, mp, paths, collector);
                } else if (fn instanceof SimpleField) {
                    if (collector.isIncluded((SimpleField) fn)) {
                        paths.add(new PathAndField(mp.immutableCopy(), (SimpleField) fn));
                    }
                }
            } while (cursor.nextSibling());
            cursor.parent();
            mp.pop();
        }
    }

    private DBObject findArrayElement(List array, List identifiers) {
        int size = ((List) array).size();
        for (int i = 0; i < size; i++) {
            DBObject elem = ((List) array).get(i);
            boolean found = true;
            for (IField ifld : identifiers) {
                Object value = getValue(elem, ifld.getPath());
                if (!((value == null && ifld.getValue() == null)
                        || (value != null && ifld.getValue() != null && value.equals(ifld.getValue())))) {
                    found = false;
                    break;
                }
            }
            if (found) {
                return elem;
            }
        }
        return null;
    }

    /**
     * Construct the initial state of Merge by going through all the fields in
     * the DBObject, and checking if they exist in metadata. Those fields that
     * don't exist in metadata will be stored in the invisibleFields list.
     */
    public void findInvisibleFields(DBObject dbObject) {
        MutablePath mp = new MutablePath();
        findInvisibleFields_dbobj(dbObject, mp);
        LOGGER.debug("Invisible fields: {} ", invisibleFields);
    }

    private void findInvisibleFields_obj(Object object,
                                         MutablePath path) {
        if (object instanceof DBObject) {
            findInvisibleFields_dbobj((DBObject) object, path);
        } else if (object instanceof List) {
            path.push(0);
            int index = 0;
            for (Object value : (List) object) {
                path.setLast(index);
                findInvisibleFields_obj(value, path);
                index++;
            }
            path.pop();
        }
    }

    private void findInvisibleFields_dbobj(DBObject dbObject,
                                           MutablePath path) {
        Set fields = dbObject.keySet();
        for (String field : fields) {
            path.push(field);
            LOGGER.debug("Processing {}", path);
            Object value = dbObject.get(field);
            try {
                md.resolve(path);
                findInvisibleFields_obj(value, path);
            } catch (Error e) {
                // Invisible field 
                LOGGER.debug("Invisible field {}", path);
                invisibleFields.add(new IField(path.immutableCopy(), value));
            }
            path.pop();
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy