Source: core/serialization/serialization.js

/**
 * @module montage/serialization/serialization
 * @requires montage/core
 */

var Montage = require("../core").Montage,
    MontageLabeler = require("./serializer/montage-labeler").MontageLabeler,
    MontageReviver = require("./deserializer/montage-reviver").MontageReviver,
    parse = require("frb/parse"),
    stringify = require("frb/stringify");

/**
 * @class Serialization
 * @extends Montage
 */
var Serialization = Montage.specialize( /** @lends Serialization.prototype # */ {
    _serializationString: {value: null},
    _serializationObject: {value: null},
    _serializationLabels: {value: null},

    initWithString: {
        value: function (string) {
            this._serializationString = string;
            this._serializationObject = null;
            this._serializationLabels = null;

            return this;
        }
    },

    initWithObject: {
        value: function (object) {
            this._serializationString = null;
            this._serializationObject = object;
            this._serializationLabels = null;

            return this;
        }
    },

    clone: {
        value: function () {
            var serialization = new Serialization();

            serialization.initWithString(this.getSerializationString());

            return serialization;
        }
    },

    getSerializationObject: {
        value: function () {
            if (!this._serializationObject) {
                this._serializationObject = JSON.parse(this._serializationString);
            }

            return this._serializationObject;
        }
    },

    getSerializationString: {
        value: function () {
            if (!this._serializationString) {
                this._serializationString = JSON.stringify(this._serializationObject);
            }

            return this._serializationString;
        }
    },

    getSerializationLabels: {
        value: function () {
            var serializationObject;

            if (!this._serializationLabels) {
                if ((serializationObject = this.getSerializationObject())) {
                    this._serializationLabels = Object.keys(serializationObject);
                }
            }

            return this._serializationLabels;
        }
    },

    getExternalObjectLabels: {
        value: function () {
            var serializationObject = this.getSerializationObject(),
                labels = [];

            for (var label in serializationObject) {
                if (Object.keys(serializationObject[label]).length === 0) {
                    labels.push(label);
                }
            }

            return labels;
        }
    },

    hasSerializationLabel: {
        value: function (label) {
            return label in this.getSerializationObject();
        }
    },

    isExternalObject: {
        value: function (label) {
            var serializationObject = this.getSerializationObject();

            if (serializationObject && label in serializationObject) {
                return Object.keys(serializationObject[label]).length === 0;
            } else {
                return false;
            }
        }
    },

    isAlias: {
        value: function (label) {
            var serializationObject = this.getSerializationObject();

            if (serializationObject && label in serializationObject) {
                return "alias" in serializationObject[label];
            } else {
                return false;
            }
        }
    },

    getElementId: {
        value: function (label) {
            var object = this.getSerializationObject();

            // TODO: much faster than using the visitor, need to make the visitor
            // faster.
            var element = Montage.getPath.call(object, label + ".values.element");

            if (!element) {
                // .properties deprecated
                element = Montage.getPath.call(object, label + ".properties.element");
            }

            if (element) {
                return element["#"];
            }
        }
    },

    getSerializationLabelsWithElements: {
        value: function (elementIds) {
            var inspector = new exports.SerializationInspector(),
                labels = [];

            inspector.initWithSerialization(this);
            inspector.visitSerialization(function (node) {
                // Check if this is one of the elements we're looking for
                if (node.type === "Element" && elementIds.indexOf(node.data) >= 0) {
                    // Check if it's inside a "values" block
                    node = node.parent;
                    // .properties deprecated
                    if (node && (node.name === "values" || node.name === "properties")) {
                        // Check if it's in a montage object
                        node = node.parent;
                        if (node && node.type === "montageObject") {
                            labels.push(node.label);
                        }
                    }
                }
            });

            return labels;
        }
    },

    renameElementReferences: {
        value: function (elementsTable) {
            var inspector = new exports.SerializationInspector();

            inspector.initWithSerialization(this);
            inspector.visitSerialization(function (node) {
                if (node.type === "Element" && node.data in elementsTable) {
                    node.data = elementsTable[node.data];
                }
            });
        }
    },

    renameSerializationLabels: {
        value: function (labelsTable) {
            var inspector = new exports.SerializationInspector();

            inspector.initWithSerialization(this);
            inspector.visitSerialization(function (node) {
                if (node.label) {
                    var label = node.label;

                    if (label in labelsTable) {
                        node.label = labelsTable[label];
                    }
                }
                if (node.type === "reference") {
                    var reference = node.data;

                    if (reference in labelsTable) {
                        node.data = labelsTable[reference];
                    }
                }
            });
        }
    },

    mergeSerialization: {
        value: function (serialization, delegate) {
            return exports.SerializationMerger.mergeSerializations(this, serialization, delegate);
        }
    },

    extractSerialization: {
        value: function (labels, externalLabels) {
            var extractor = new exports.SerializationExtractor();

            extractor.initWithSerialization(this);
            return extractor.extractSerialization(labels, externalLabels);
        }
    }
});

/**
 * @class SerializationMerger
 * @extends Montage
 */
var SerializationMerger = Montage.specialize(null, /** @lends SerializationMerger.prototype */ {
    /**
     * This delegate method is called when merging an object from serialization2
     * into serialization1. It allows the delegate to change how the object is
     * going to be merged by saying that the object already exists in
     * serialization1 under a different or the same label.
     *
     * When the delegate method doesn't return a string then the default
     * behavior is to add a new object to serialization1. If the object's label
     * collides with another label in serialization1 then a new label is
     * generated and used.
     *
     * By returning a label that exists in serialization1 from the delegate
     * method all references to the object being merged will change to point
     * to the object from serialization1 instead and the object will not be
     * merged.
     * By returning a label that does not exist in both serializations the
     * object will be merged into serialization2 with this new label instead.
     *
     * @callback delegateWillMergeObjectWithLabel
     * @param {string} label The object label.
     * @param {string} newLabel The new label generated by the collision resolver in case of collision.
     * @returns {string|undefined} the new label for this object.
     */

    /**
     * Merges serialization2 into serialization1.
     *
     * @param {Serialization} serialization1 - The serialization to be merged and end result of the merge operation.
     * @param {Serialization} serialization2 - The serialization to be merged.
     * @param {{willMergeObjectWithLabel: delegateWillMergeObjectWithLabel}} delegate
     * - The delegate to override the default behavior.
     *
     * @returns {Object} The collision table with the new labels generated for label clashes
     */
    mergeSerializations: {
        value: function (serialization1, serialization2, delegate) {
            var serializationObject1,
                serializationObject2,
                labels1,
                labels2,
                collisionTable = {},
                foundCollisions,
                hasCollisionTableChanged;

            labels1 = serialization1.getSerializationLabels();
            labels2 = serialization2.getSerializationLabels();

            // Check for name collisions and generate new labels
            foundCollisions = this._createCollisionTable(labels1, labels2, collisionTable, delegate && delegate.labeler);

            if (delegate && delegate.willMergeObjectWithLabel) {
                hasCollisionTableChanged = this._willMergeObjectWithLabel(delegate, serialization1, serialization2, collisionTable);
                foundCollisions = foundCollisions || hasCollisionTableChanged;
            }

            // Replace the labels with the new, non-colliding, ones
            if (foundCollisions) {
                // Clone serialization2 because we don't want to modify it.
                serialization2 = serialization2.clone();
                serialization2.renameSerializationLabels(collisionTable);
                labels2 = serialization2.getSerializationLabels();
            }

            // Merge the two serializations without the fear of name clashing
            serializationObject1 = serialization1.getSerializationObject();
            serializationObject2 = serialization2.getSerializationObject();

            for (var i = 0, label; (label = labels2[i]); i++) {
                // If this label already exists in serialization1 don't merge
                // it as it means they are the same object.
                if (labels1.indexOf(label) === -1) {
                    serializationObject1[label] = serializationObject2[label];
                }
            }

            serialization1.initWithObject(serializationObject1);

            return collisionTable;
        }
    },

    /**
     * @private
     * @function
     */
    _willMergeObjectWithLabel: {
        value: function (delegate, serialization1, serialization2, collisionTable) {
            var newLabel,
                collisionLabel,
                collisionLabels,
                inDestination,
                renameLabel,
                hasCollisionTableChanged = false,
                labels2 = serialization2.getSerializationLabels();

            if (collisionTable) {
                collisionLabels = [];
                Object.keys(collisionTable).forEach(function (label) {
                    collisionLabels.push(collisionTable[label]);
                });
            }

            for (var i = 0, label; (label = labels2[i]); i++) {
                collisionLabel = collisionTable && collisionTable[label];
                newLabel = delegate.willMergeObjectWithLabel(label, collisionLabel);

                if (typeof newLabel === "string") {
                    // If the delegate returns a new label there are two
                    // possible interpretations:
                    // 1) The label is on the destination serialization so it
                    //    means that we don't need to move this object there
                    //    because it is already there under a different label.
                    //    We just need to update the references to point to the
                    //    right name.
                    // 2) The label doesn't exist anywhere so it means we just
                    //    need to move the object under this new different
                    //    label.
                    inDestination = this._isLabelValidInSerialization(
                        newLabel, serialization1);
                    if (!inDestination) {
                        renameLabel = !this._isLabelValidInSerialization(newLabel, serialization2) && 
                                        collisionLabels.indexOf(newLabel) === -1;
                    }

                    if (inDestination || renameLabel) {
                        hasCollisionTableChanged = true;
                        collisionTable[label] = newLabel;
                    } else {
                        throw new Error("willMergeObjectWithLabel either needs to return a label that exists in the destination serialization to indicate it's the same object or return a completely new label to rename the object being merged. \"" + newLabel + "\"  destination: " + serialization1.getSerializationString() + "\n source: " + serialization2.getSerializationString() + "\n collision table: " + JSON.stringify(collisionTable, null, 4));
                    }
                }
            }

            return hasCollisionTableChanged;
        }
    },

    /**
     * This function returns true when the label is part of the serialization,
     * or, if a template property, it refers to a component label that is part
     * of the serialization.
     *
     * @private
     */
    _isLabelValidInSerialization: {
        value: function (label, serialization) {
            var componentLabel,
                ix;

            if (serialization.hasSerializationLabel(label)) {
                return true;
            } else {
                ix = label.indexOf(":");
                // It's a template property, if the component part is
                // in the serialization then it's a valid label too.
                if (ix > 0) {
                    componentLabel = label.slice(0, ix);
                    if (serialization.hasSerializationLabel(componentLabel)) {
                        return true;
                    }
                }
            }

            return false;
        }
    },

    /**
     * This function creates a collision table between labels1 and labels2.
     * The collision table offers renames for the labels in the labels2 array
     * that already exist in the labels1 array.
     *
     * This function knows how to deal with labels that refer to template
     * values. A label for a template property has the following syntax:
     * <component label>:<label>.
     * The collision table guarantees that template values' labels will
     * always be in sync with their corresponding component label.
     *
     * When a collision exist with a label for a template property the
     * collision is solved by creating a new label for the component and
     * adopting that new label: <new component label>:<label>. In this
     * case the component label will also be part of the resulting collision
     * table even if there was no original collision in labels1.
     *
     * @example
     * labels1: ["repetition:iteration"]
     * labels2: ["repetition", "repetition:iteration"]
     * collisionTable: {"repetition:iteration": "object:iteration",
     *                  "repetition": "object"}
     *
     * @private
     */
    _createCollisionTable: {
        value: function (labels1, labels2, collisionTable, labeler) {
            
            labeler = labeler || new MontageLabeler();
            var foundCollisions = false,
                componentLabel,
                labels1Index = Object.create(null),
                newLabel,
                label,
                ix, i;

            for (i = 0; i < labels1.length; i++) {
                label = labels1[i];

                // If this label is a property template then we need to register
                // the component name too, it could be that it's not present
                // on labels1. We want to avoid the possibility of generating
                // a label that conflicts with the component part of the template
                // property.
                ix = label.indexOf(":");
                if (ix > 0) {
                    componentLabel = label.slice(0, ix);
                    labeler.addLabel(componentLabel);
                    labels1Index[componentLabel] = 1;
                }
                labeler.addLabel(label);
                labels1Index[label] = 1;
            }

            for (i = 0; (label = labels2[i]); i++) {
                // If the label is a template property then check to see if
                // the component label has been renamed already or if the entire
                // label or component label have a collision to solve.
                ix = label.indexOf(":");
                if (ix > 0) {
                    componentLabel = label.slice(0, ix);
                    newLabel = collisionTable[componentLabel];

                    if (newLabel) {
                        collisionTable[label] = newLabel + ":" + label.slice(ix+1);
                        foundCollisions = true;
                    } else if (componentLabel in labels1Index) {
                        // Renaming a label that is a property template is
                        // the same as renaming the component part of the
                        // label.
                        newLabel = labeler.generateLabel(labeler.getLabelBaseName(componentLabel));
                        // Rename the component label too if it exists.
                        if (labels2.indexOf(componentLabel) >= 0) {
                            collisionTable[componentLabel] = newLabel;
                        }
                        collisionTable[label] = newLabel + label.slice(ix);
                        foundCollisions = true;
                    } else {
                        labeler.addLabel(componentLabel);
                    }
                }
                // Also check if the label already has a new label, this can
                // happen if a template property on that component was renamed.
                else if (label in labels1Index && !(label in collisionTable)) {
                    collisionTable[label] = labeler.generateLabel(labeler.getLabelBaseName(label));
                    foundCollisions = true;
                } else {
                    labeler.addLabel(label);
                }
            }

            return foundCollisions;
        }
    }
});


/**
 * @class SerializationInspector
 * @extends Montage
 */
var SerializationInspector = Montage.specialize(/** @lends SerializationInspector.prototype # */ {
    initWithSerialization: {
        value: function (serialization) {
            this._serialization = serialization;
        }
    },

    visitSerialization: {
        value: function (visitor) {
            var serialization = this._serialization.getSerializationObject();

            this._walkRootObjects(visitor, serialization);
            this._serialization.initWithObject(serialization);
        }
    },

    visitSerializationObject: {
        value: function (label, visitor) {
            var serialization = this._serialization.getSerializationObject();

            if (label in serialization) {
                this._walkRootObject(visitor, serialization, label);
                this._serialization.initWithObject(serialization);
            } else {
                throw new Error('Object "' + label + '" does not exist in ' + this._serialization.getSerializationString());
            }
        }
    },

    changeLabel: {
        value: function (oldLabel, newLabel) {
            var serialization = this._serialization.getSerializationObject(),
                object;

            object = serialization[oldLabel];
            delete serialization[oldLabel];
            serialization[newLabel] = object;
        }
    },

    _walkRootObjects: {
        value: function (visitor, objects) {
            /* jshint forin: true */
            for (var label in objects) {                
            /* jshint forin: false */
                this._walkRootObject(visitor, objects, label);
            }
        }
    },

    _walkRootObject: {
        value: function (visitor, objects, label) {
            var object = objects[label];
            /* jshint forin: true */
            if ("value" in object) {
            /* jshint forin: false */
                this._walkObject(visitor, object, "value", label);
            } else {
                this._walkCustomObject(visitor, objects, label);
            }
        }
    },

    /**
     * @private
     * @param parentObject {Object} The parent object of the object to walk
     * @param key {string} The key of the object in the parent object
     * @param label {string} Optional label for when the object has no parent
     * @param parent {Object} The representation of the object's parent
     */
    _walkObject: {
        value: function (visitor, parentObject, key, label, parent) {
            var object = parentObject[key],
                type = MontageReviver.getTypeOf(object),
                value;

            // Create the value representing this object in the serialization.
            value = {
                type: type
            };
            if (label) {
                value.label = label;
            } else {
                value.name = key;
            }
            if (parent) {
                value.parent = parent;
            }

            // Visit the value
            if (type === "number" || type === "string" || type === "null") {
                value.data = object;
                visitor(value);
                parentObject[key] = value.data;

            } else if (type === "regexp") {
                value.data = object["/"];
                visitor(value);
                object["/"] = value.data;

            } else if (type === "reference") {
                value.data = object["@"];
                visitor(value);
                object["@"] = value.data;

            } else if (type === "Element") {
                value.data = object["#"];
                visitor(value);
                object["#"] = value.data;

            } else if (type === "array") {
                value.data = object;
                visitor(value);
                parentObject[key] = object = value.data;

                for (var i = 0, ii = object.length; i < ii; i++) {
                    this._walkObject(visitor, object, ""+i, null, value);
                }

            } else if (type === "binding") {
                this._walkBinding(visitor, parentObject, key, value);

            } else if (type === "object") {
                value.data = object;
                visitor(value);
                parentObject[key] = object = value.data;

                /* jshint forin: true */
                for (var prop in object) {
                /* jshint forin: false */
                    this._walkObject(visitor, object, prop, null, value);
                }
            }

            if (label !== null && label !== undefined && label !== value && value.label !== label) {
                this.changeLabel(label, value.label);
            }
        }
    },

    _walkCustomObject: {
        value: function (visitor, objects, label) {
            var object = objects[label],
                value;

            value = {
                type: "montageObject",
                label: label,
                data: object
            };

            visitor(value);
            objects[label] = object = value.data;
            if (value.label !== label) {
                this.changeLabel(label, value.label);
            }

            if (object.values) {
                this._walkObject(visitor, object, "values", null, value);
            } else if (object.properties) { // deprecated
                this._walkObject(visitor, object, "properties", null, value);
            }
            if (object.bindings) { // deprecated
                this._walkBindings(visitor, object, null, value);
            }
            if (object.listeners) {
                this._walkObject(visitor, object, "listeners", null, value);
            }
            if (object.localizations) {
                this._walkLocalizations(visitor, object, null, value);
            }
        }
    },

    _walkBindings: {
        value: function (visitor, parentObject, parent) {
            var object = parentObject.bindings,
                value;

            value = {
                type: "bindings",
                data: object,
                parent: parent
            };

            visitor(value);
            parentObject.bindings = object = value.data;

            /* jshint forin: true */
            for (var key in object) {
            /* jshint forin: false */
                this._walkBinding(visitor, object, key, value);
            }
        }
    },

    _walkBinding: {
        value: function (visitor, parentObject, key, parent) {
            var object = parentObject[key],
                value;

            value = {
                type: "binding",
                name: key,
                data: object,
                parent: parent
            };

            visitor(value);
            parentObject[key] = object = value.data;

            this._walkBindingData(visitor, object, value);
        }
    },

    _walkBindingData: {
        value: function (visitor, object, parent) {
            var sourcePath,
                parseTree,
                modified = false;

            sourcePath = object["<-"] || object["<->"];
            parseTree = Object.clone(parse(sourcePath));
            this._walksBindingReferences(parseTree, function (syntax) {
                var value = {
                    type: "reference",
                    data: syntax.label
                };
                visitor(value);
                if (syntax.label !== value.data) {
                    syntax.label = value.data;
                    modified = true;
                }
            });

            if (modified) {
                if ("<-" in object) {
                    object["<-"] = stringify(parseTree);
                } else {
                    object["<->"] = stringify(parseTree);
                }
            }

            if (object.converter) {
                this._walkObject(visitor, object, "converter", null, parent);
            }
        }
    },

    _walkLocalizations: {
        value: function (visitor, parentObject, parent) {
            var object = parentObject.localizations,
                value;

            value = {
                type: "localizations",
                data: object,
                parent: parent
            };

            visitor(value);
            parentObject.localizations = object = value.data;

            /* jshint forin: true */
            for (var key in object) {
            /* jshint forin: false */
                this._walkLocalization(visitor, object, key, value);
            }
        }
    },

    _walkLocalization: {
        value: function (visitor, parentObject, key, parent) {
            var object = parentObject[key],
                value,
                data;

            value = {
                type: "localization",
                name: key,
                data: object,
                parent: parent
            };

            visitor(value);
            parentObject[key] = object = value.data;

            if (typeof object.key === "object") {
                this._walkBindingData(visitor, object.key, value);
            }

            if (typeof object.default === "object") {
                this._walkBindingData(visitor, object.default, value);
            }

            if (typeof object.data === "object") {
                data = object.data;

                /* jshint forin: true */
                for (var prop in data) {
                /* jshint forin: false */
                    this._walkBindingData(visitor, data[prop], value);
                }
            }
        }
    },

    /**
     * Visits all object references made in the binding parsing tree
     * @private
     */
    _walksBindingReferences: {
        value: function (parseTree, visitor) {
            var args = parseTree.args;

            if (parseTree.type === "component") {
                visitor(parseTree);
            }

            if (args) {
                for (var i = 0, ii = args.length; i < ii; i++) {
                    this._walksBindingReferences(args[i], visitor);
                }
            }
        }
    }
});


/**
 * @class SerializationExtractor
 * @extends Montage
 */
var SerializationExtractor = Montage.specialize( /** @lends SerializationExtractor.prototype # */ {
    /**
     * @private
     */
    _serialization: {value: null},

    /**
     * @function
     */
    initWithSerialization: {
        value: function (serialization) {
            this._serialization = serialization;
        }
    },

    /**
     * Creates a new serialization with the labels given.
     * @function
     */
    extractSerialization: {
        value: function (labels, externalLabels) {

            var i, label,
                inspector = new exports.SerializationInspector(),
                serializationObject,
                objects = {},
                references = [];

            serializationObject = this._serialization.getSerializationObject();
            inspector.initWithSerialization(this._serialization);

            for (i = 0, label; (label = labels[i]); i++) {
                objects[label] = serializationObject[label];

                inspector.visitSerializationObject(label, function (node) { // jshint ignore:line
                    var label;

                    if (node.type === "reference") {
                        label = node.data;
                        // We don't process template values here, meaning
                        // that if we have "table" and a reference like
                        // "@table:cell" the latter will be considered an
                        // external reference even though the component is in
                        // scope.
                        // We do this on purpose because it allow us to process
                        // all template values of the serialization without
                        // having to walk the entire serialization tree looking
                        // for them.
                        // If for some reason we need to "correct" this behavior
                        // then we also need to change the way we resolve
                        // template values' alias in
                        // Template.expandParameters.
                        // Instead of relying on willMergeObjectWithLabel we
                        // need to walk the serialization looking for these.
                        if (references.indexOf(label) === -1 &&
                            labels.indexOf(label) === -1) {
                            references.push(label);
                        }
                    }
                });
            }

            if (externalLabels) {
                for (i = 0, label; (label = externalLabels[i]); i++) {
                    // Make sure we don't add objects that are not part of the
                    // serialization we're extracting from.
                    // If the same label is defined in both labels and
                    // externalLabels then labels takes precedence.
                    if (label in serializationObject && !(label in objects)) {
                        objects[label] = {};
                    }
                }
            }

            for (i = 0, label; (label = references[i]); i++) {
                objects[label] = {};
            }

            return new Serialization().initWithObject(objects);
        }
    }
});

exports.Serialization = Serialization;
exports.SerializationMerger = SerializationMerger;
exports.SerializationInspector = SerializationInspector;
exports.SerializationExtractor = SerializationExtractor;