<commit_before>(function (ns) {

    var common = require("./../../base/common.js"),
        Shade = require("../../interfaces.js"),
        evaluator = require("../constants/evaluator.js"),
        estraverse = require('estraverse');

    var codegen = require('escodegen');

    var Syntax = common.Syntax,
        TYPES = Shade.TYPES,
        ANNO = common.ANNO;

    var debug = false;

    var generateErrorInformation = function() {
        var args = Array.prototype.slice.call(arguments);
        var node = args.shift(),
            loc = node.loc,
            msg = "";

        if (loc && loc.start.line) {
            msg = ", Line " + loc.start.line;
        }
        msg += ": " + codegen.generate(node);
        return { message: args.join(" ") + msg, loc: loc};
    };

    var handlers = {

        /**
         * @param node
         */
        Literal: function (node) {
            var value = node.raw !== undefined ? node.raw : node.value,
                result = ANNO(node);

            var number = parseFloat(value);
            if (!isNaN(number)) {
                if (value.toString().indexOf(".") == -1) {
                    result.setType(TYPES.INT);
                }
                else {
                    result.setType(TYPES.NUMBER);
                }
            } else if (value === 'true' || value === 'false') {
                result.setType(TYPES.BOOLEAN);
            } else if (value === 'null') {
                result.setType(TYPES.NULL);
            } else {
                result.setType(TYPES.STRING);
            }
            if (!result.isNull()) {
                result.setStaticValue(evaluator.getStaticValue(node));
            }
        },

        /**
         * ExpressionStatement: Just copy the result from the actual expression
         */
        ExpressionStatement: function (node) {
            var result = ANNO(node),
                expression = ANNO(node.expression);
            result.copy(expression);
        },

        /**
         * ReturnStatement: If return has an argument, copy the TypeInfo
         * form the argument, otherwise it's undefined. Inform the scope on
         * the return type of this return branch.
         */
        ReturnStatement: function (node, parent, context) {
            var result = ANNO(node),
                argument = context.getTypeInfo(node.argument);

            if (argument) {
                result.copy(argument);
            } else {
                result.setType(TYPES.UNDEFINED);
            }
            context.getScope().updateReturnInfo(result);
        },

        /**
         * NewExpression: Find the type of the Callee from
         * the scope and evaluate based on annotated parameters
         */
        NewExpression: function(node, parent, context) {
            var result = ANNO(node), staticValue;

            var scope = context.getScope();
            var entry = scope.getBindingByName(node.callee.name);
            if (entry && entry.hasConstructor()) {
                var constructor = entry.getConstructor();
                var args = context.getTypeInfo(node.arguments);
                try {
                    var extra = constructor.evaluate(result, args, scope);
                    result.setFromExtra(extra);
                } catch (e) {
                    result.setInvalid(e);
                }
                if (constructor.computeStaticValue) {
                    try {
                        staticValue = constructor.computeStaticValue(result, context.getTypeInfo(node.arguments), scope);
                        if (staticValue !== undefined) {
                            result.setStaticValue(staticValue);
                        }
                    } catch (e) {
                        result.setDynamicValue();
                    }
                }
            }
            else {
                result.setInvalid(generateErrorInformation(node, "ReferenceError: " + node.callee.name + " is not defined"));
            }
        },

        /**
         * UnaryExpression
         */
        UnaryExpression: function (node, parent, context) {
            var result = ANNO(node),
                argument = context.getTypeInfo(node.argument),
                operator = node.operator;

            //noinspection FallthroughInSwitchStatementJS
            switch (operator) {
                case "!":
                    result.setType(TYPES.BOOLEAN);
                    if (argument.canObject()) {
                        result.setStaticValue(false); // !obj == false
                        return;
                    }
                    break;
                case "+":
                case "-":
                    if (argument.canInt()) {
                        result.setType(TYPES.INT);
                    } else if (argument.canNumber()) {
                        result.setType(TYPES.NUMBER);
                    } else {
                        result.setInvalid(generateErrorInformation(node, "NotANumberError"));
                    }
                    break;
                case "~":
                case "typeof":
                case "void":
                case "delete":
                default:
                    result.setInvalid(generateErrorInformation(node, "NotSupportedError"));
            }
            if (argument.hasStaticValue()) {
                result.setStaticValue(evaluator.getStaticValue(node));
            } else {
                result.setDynamicValue();
            }
        },

        /**
         * 'Undefined' is an identifier. Variables, names of functions and
         * member properties are handled within parent expressions
         */
        Identifier: function (node) {
            if (node.name === "undefined") {
                ANNO(node).setType(TYPES.UNDEFINED);
            }
        },

        /**
         * BinaryExpression
         */
        BinaryExpression: function (node, parent, context) {
            //console.log(node.left, node.right);
            var left = context.getTypeInfo(node.left),
                right = context.getTypeInfo(node.right),
                result = ANNO(node),
                operator = node.operator;

            if(!(left.isValid() && right.isValid())) {
                result.setInvalid()
                return;
            }

            //noinspection FallthroughInSwitchStatementJS
            switch (operator) {
                case "+":
                case "-":
                case "*":
                case "/":
                case "%":
                    // int 'op' int => int
                    // int / int => number
                    if (left.canInt() && right.canInt()) {
                        if (operator == "/")
                            result.setType(TYPES.NUMBER);
                        else
                            result.setType(TYPES.INT);
                    }
                    // int 'op' number => number
                    else if (left.canInt() && right.isNumber() || right.canInt() && left.isNumber()) {
                        result.setType(TYPES.NUMBER);
                    }
                    // number 'op' number => number
                    else if (left.isNumber() && right.isNumber()) {
                        result.setType(TYPES.NUMBER);
                        // int 'op' null => int
                    }
                    else if (left.isInt() && right.isNull() || right.isInt() && left.isNull()) {
                        result.setType(TYPES.INT);
                    }
                    // number 'op' null => number
                    else if ((left.isNumber() && right.isNull()) || (right.isNumber() && left.isNull())) {
                        result.setType(TYPES.NUMBER);
                    }
                    else {
                        // NaN
                        result.setInvalid(generateErrorInformation(node, "NotANumberError"));
                    }
                    break;
                case "===":
                case "!==":
                    result.setType(TYPES.BOOLEAN);
                    if (left.isUndefined() || right.isUndefined()) {
                        var value = left.isUndefined() && right.isUndefined();
                        result.setStaticValue(operator == "===" ? value : !value);
                        return;
                    }
                    break;
                case "==": // comparison
                case "!=":
                case ">":
                case "<":
                case ">=":
                case "<=":
                    result.setType(TYPES.BOOLEAN);
                    if (left.isUndefined() || right.isUndefined()) {
                        var value = left.isUndefined() && right.isUndefined();
                        result.setStaticValue(operator == "!=" ? !value : value);
                        return;
                    }
                    break;
                default:
                    throw new Error("Operator not supported: " + operator);
            }
             if (left.hasStaticValue() && right.hasStaticValue()) {
                //console.log(left.getStaticValue(), operator, right.getStaticValue());
                result.setStaticValue(evaluator.getStaticValue(node));
            } else {
                result.setDynamicValue();
            }
        },

        AssignmentExpression: function (node, parent, context) {
            var right = context.getTypeInfo(node.right),
                result = ANNO(node);

            result.copy(right);
            result.setDynamicValue();

            // Check, if a assigned variable still has the same type as
            // before and update type of uninitialized variables.
            if (node.left.type == Syntax.Identifier && !context.inDeclaration() && right.isValid()) {
                var name = node.left.name;
                var scope = context.getScope();
                scope.updateTypeInfo(name, right);
            }
        },

        MemberExpression: function (node, parent, context) {
            var resultType = context.getTypeInfo(node),
                objectAnnotation = ANNO(node.object),
                propertyAnnotation = ANNO(node.property),
                scope = context.getScope();

            if(!objectAnnotation.isValid()) {
                resultType.setInvalid();
                return;
            }

            //console.log("Member", node.object.name, node.property.name);
            if (node.computed) {
                if (objectAnnotation.isArray()) {
                    // Property is computed, thus it could be a variable
                    var propertyType =  context.getTypeInfo(node.property);
                    if (!propertyType.canInt()) {
                        Shade.throwError(node, "Expected 'int' type for array accessor");
                    }
                    var elementInfo = objectAnnotation.getArrayElementType();
                    resultType.setType(elementInfo.type, elementInfo.kind);
                    return;
                }
                else {
                    resultType.setInvalid(generateErrorInformation(node, "Cannot access member via computed value from object", objectAnnotation.getTypeString()));

                    //Shade.throwError(node, "TypeError: Cannot access member via computed value from object '" + objectAnnotation.getTypeString());
                }
            }
            var propertyName = node.property.name;

            var objectOfInterest = common.getObjectReferenceFromNode(node.object, scope);

            objectOfInterest || Shade.throwError(node,"ReferenceError: " + node.object.name + " is not defined. Context: " + scope.str());

            if (!objectOfInterest.isValid() || objectOfInterest.getType() == TYPES.UNDEFINED) {  // e.g. var a = undefined; a.unknown;
                resultType.setInvalid(generateErrorInformation(node, "TypeError: Cannot read property '" + propertyName + "' of undefined"));
                return;
            }
            if (objectOfInterest.getType() != TYPES.OBJECT) { // e.g. var a = 5; a.unknown;
                resultType.setType(TYPES.UNDEFINED);
                return;
            }

            var objectInfo = scope.getObjectInfoFor(objectOfInterest);
            if(!objectInfo)
                Shade.throwError(node, "Internal: Incomplete registration for object: " + objectOfInterest.getTypeString() + ", " + JSON.stringify(node.object));

            objectAnnotation.copy(objectOfInterest);
            if (!objectInfo.hasOwnProperty(propertyName)) {
                resultType.setType(TYPES.UNDEFINED);
                propertyAnnotation.setType(TYPES.UNDEFINED);
                return;
            }

            var propertyTypeInfo = objectInfo[propertyName];
            propertyAnnotation.setFromExtra(propertyTypeInfo);
            resultType.setFromExtra(propertyTypeInfo);
        },

        CallExpression: function (node, parent, context) {
            var result = ANNO(node),
                scope = context.getScope(),
                args = context.getTypeInfo(node.arguments),
                extra, staticValue;

            if (!args.every(function (arg) {return arg.isValid() })) {
                result.setInvalid(generateErrorInformation(node, "Not all arguments types of call expression could be evaluated"));
                return;
            }

            // Call on an object, e.g. Math.cos()
            if (node.callee.type == Syntax.MemberExpression) {

                var memberExpression = context.getTypeInfo(node.callee);
                if(!memberExpression.isValid()) {
                    result.setInvalid();
                    return;
                }

                var object = node.callee.object,
                    propertyName = node.callee.property.name;

                var objectReference = context.getTypeInfo(object);
                if(!objectReference)  {
                    Shade.throwError(node, "Internal: No object info for: " + object);
                }
                var objectInfo = scope.getObjectInfoFor(objectReference);
                if(!objectInfo) { // Every object needs an info, otherwise we did something wrong
                    Shade.throwError(node, "Internal Error: No object registered for: " + objectReference.getTypeString() + JSON.stringify(node.object));
                }

                if (!memberExpression.isFunction()) { // e.g. Math.PI()
                    result.setInvalid();
                    if (objectInfo.hasOwnProperty(propertyName)) {
                      result.setError(generateErrorInformation(node, "TypeError: Property '" + propertyName + "' of object #<"+ objectReference.getTypeString() +"> is not a function"));
                    } else {
                      result.setError(generateErrorInformation(node, "TypeError: " + (object.type == Syntax.ThisExpression ? "'this'" : objectReference.getTypeString())+ " has no method '"+ propertyName + "'"));
                    }
                    return;
                }

                if (!objectInfo.hasOwnProperty(propertyName)) {
                    result.setType(TYPES.UNDEFINED);
                    return;
                }
                var propertyHandler = objectInfo[propertyName];

                if (typeof propertyHandler.evaluate != "function") {
                    Shade.throwError(node, "Internal: no handler registered for '" + propertyName + "'");
                }
                // Evaluate type of call

                try {
                    extra = propertyHandler.evaluate(result, args, scope, objectReference, context);
                    result.setFromExtra(extra);
                } catch (e) {
                    result.setInvalid(generateErrorInformation(node, e.message));
                    return;
                }

                // If we have a type, evaluate static value
                if (typeof propertyHandler.computeStaticValue != "function") {
                    debug && console.warn("No static evaluation exists for function", codegen.generate(node));
                    return;
                }
                staticValue = propertyHandler.computeStaticValue(result, args, scope, objectReference, context);
                if (staticValue !== undefined) {
                    result.setStaticValue(staticValue);
                }
                return;

            }  else if (node.callee.type == Syntax.Identifier) {
                var functionName = node.callee.name;
                var func = scope.getBindingByName(functionName);
                if (!func) {
                    Shade.throwError(node, "ReferenceError: " + functionName + " is not defined");
                }
                if(!func.isFunction()) {
                    Shade.throwError(node, "TypeError: " + func.getTypeString() + " is not a function");
                }
                try {
                    extra = context.callFunction(scope.getVariableIdentifier(functionName), args);
                    extra && result.setFromExtra(extra);
                } catch(e) {
                    result.setInvalid(generateErrorInformation(node, "Failure in function call: ", e.msg));
                }
                return;
            }

            throw new Error("Unhandled CallExpression:" + node.callee.type);

        },

        VariableDeclaration: function (node, parent, context) {
            context.setInDeclaration(false);
        },

        LogicalExpression: function (node, parent, context) {
            var left = context.getTypeInfo(node.left),
                right = context.getTypeInfo(node.right),
                result = ANNO(node);

            // static: true || false, dynamic: undefined
            var leftBool = left.getStaticTruthValue(),
                rightBool = right.getStaticTruthValue(),
                operator = node.operator;

            if (operator === "||") {
                if (leftBool === false) {
                    result.copy(right);
                    return;
                }
                if (leftBool === true) {
                    result.copy(left);
                    return;
                }
                // Left is dynamic, let's check right
                if (rightBool === false) {
                    // Now the result type is always the one of the left value
                    result.copy(left);
                    return;
                }
            } else if (operator === "&&") {
                if (leftBool === false) {
                    // T(x) == false => x && y == x
                    result.copy(left);
                    return;
                }
                if (leftBool === true) {
                    result.copy(right);
                    return;
                }
                // Left is dynamic, let's check right
                if (rightBool === true) {
                    // Now the result type is always the one of the left value
                    result.copy(left);
                    return;
                }
                if (rightBool === false) {
                    // Now the result must be false
                    result.setType(TYPES.BOOLEAN);
                    result.setStaticValue(false);
                    return;
                }
            }

            // If we can cast both sides to a common type, it's fine
            if(result.setCommonType(left, right)) {
                return;
            }

            result.setInvalid(generateErrorInformation(node, "Can't evaluate polymorphic logical expression"))

        },

        ConditionalExpression: function (node, parent, context) {
            var consequent = context.getTypeInfo(node.consequent),
                alternate = context.getTypeInfo(node.alternate),
                test = context.getTypeInfo(node.test),
                result = ANNO(node);

            if (consequent.equals(alternate)) {
                result.copy(consequent);
            } else if (consequent.canNumber() && alternate.canNumber()) {
                result.setType(TYPES.NUMBER);
            } else if(test.isNullOrUndefined()) {
                result.copy(alternate);
            }

        }

    };

    ns.annotateRight  = function(ast) {

        if(!ast)
            throw Error("No node to analyze");

        var controller = new estraverse.Controller();
        var context = this;

        controller.traverse(ast, {
            enter: function(node) {
                if(node.type == Syntax.VariableDeclaration) {
                    context.setInDeclaration(true);
                }
            },
            leave: function(node, parent) {
                if (handlers.hasOwnProperty(node.type)) {
                    return handlers[node.type].call(this, node, parent, context);
                }
                return null;
            }
        });

    }
}(exports));
<commit_msg>TypeInference: Bugfix in ConditionalExpression<commit_after>(function (ns) {

    var common = require("./../../base/common.js"),
        Shade = require("../../interfaces.js"),
        evaluator = require("../constants/evaluator.js"),
        estraverse = require('estraverse');

    var codegen = require('escodegen');

    var Syntax = common.Syntax,
        TYPES = Shade.TYPES,
        ANNO = common.ANNO;

    var debug = false;

    var generateErrorInformation = function() {
        var args = Array.prototype.slice.call(arguments);
        var node = args.shift(),
            loc = node.loc,
            msg = "";

        if (loc && loc.start.line) {
            msg = ", Line " + loc.start.line;
        }
        msg += ": " + codegen.generate(node);
        return { message: args.join(" ") + msg, loc: loc};
    };

    var handlers = {

        /**
         * @param node
         */
        Literal: function (node) {
            var value = node.raw !== undefined ? node.raw : node.value,
                result = ANNO(node);

            var number = parseFloat(value);
            if (!isNaN(number)) {
                if (value.toString().indexOf(".") == -1) {
                    result.setType(TYPES.INT);
                }
                else {
                    result.setType(TYPES.NUMBER);
                }
            } else if (value === 'true' || value === 'false') {
                result.setType(TYPES.BOOLEAN);
            } else if (value === 'null') {
                result.setType(TYPES.NULL);
            } else {
                result.setType(TYPES.STRING);
            }
            if (!result.isNull()) {
                result.setStaticValue(evaluator.getStaticValue(node));
            }
        },

        /**
         * ExpressionStatement: Just copy the result from the actual expression
         */
        ExpressionStatement: function (node) {
            var result = ANNO(node),
                expression = ANNO(node.expression);
            result.copy(expression);
        },

        /**
         * ReturnStatement: If return has an argument, copy the TypeInfo
         * form the argument, otherwise it's undefined. Inform the scope on
         * the return type of this return branch.
         */
        ReturnStatement: function (node, parent, context) {
            var result = ANNO(node),
                argument = context.getTypeInfo(node.argument);

            if (argument) {
                result.copy(argument);
            } else {
                result.setType(TYPES.UNDEFINED);
            }
            context.getScope().updateReturnInfo(result);
        },

        /**
         * NewExpression: Find the type of the Callee from
         * the scope and evaluate based on annotated parameters
         */
        NewExpression: function(node, parent, context) {
            var result = ANNO(node), staticValue;

            var scope = context.getScope();
            var entry = scope.getBindingByName(node.callee.name);
            if (entry && entry.hasConstructor()) {
                var constructor = entry.getConstructor();
                var args = context.getTypeInfo(node.arguments);
                try {
                    var extra = constructor.evaluate(result, args, scope);
                    result.setFromExtra(extra);
                } catch (e) {
                    result.setInvalid(e);
                }
                if (constructor.computeStaticValue) {
                    try {
                        staticValue = constructor.computeStaticValue(result, context.getTypeInfo(node.arguments), scope);
                        if (staticValue !== undefined) {
                            result.setStaticValue(staticValue);
                        }
                    } catch (e) {
                        result.setDynamicValue();
                    }
                }
            }
            else {
                result.setInvalid(generateErrorInformation(node, "ReferenceError: " + node.callee.name + " is not defined"));
            }
        },

        /**
         * UnaryExpression
         */
        UnaryExpression: function (node, parent, context) {
            var result = ANNO(node),
                argument = context.getTypeInfo(node.argument),
                operator = node.operator;

            //noinspection FallthroughInSwitchStatementJS
            switch (operator) {
                case "!":
                    result.setType(TYPES.BOOLEAN);
                    if (argument.canObject()) {
                        result.setStaticValue(false); // !obj == false
                        return;
                    }
                    break;
                case "+":
                case "-":
                    if (argument.canInt()) {
                        result.setType(TYPES.INT);
                    } else if (argument.canNumber()) {
                        result.setType(TYPES.NUMBER);
                    } else {
                        result.setInvalid(generateErrorInformation(node, "NotANumberError"));
                    }
                    break;
                case "~":
                case "typeof":
                case "void":
                case "delete":
                default:
                    result.setInvalid(generateErrorInformation(node, "NotSupportedError"));
            }
            if (argument.hasStaticValue()) {
                result.setStaticValue(evaluator.getStaticValue(node));
            } else {
                result.setDynamicValue();
            }
        },

        /**
         * 'Undefined' is an identifier. Variables, names of functions and
         * member properties are handled within parent expressions
         */
        Identifier: function (node) {
            if (node.name === "undefined") {
                ANNO(node).setType(TYPES.UNDEFINED);
            }
        },

        /**
         * BinaryExpression
         */
        BinaryExpression: function (node, parent, context) {
            //console.log(node.left, node.right);
            var left = context.getTypeInfo(node.left),
                right = context.getTypeInfo(node.right),
                result = ANNO(node),
                operator = node.operator;

            if(!(left.isValid() && right.isValid())) {
                result.setInvalid()
                return;
            }

            //noinspection FallthroughInSwitchStatementJS
            switch (operator) {
                case "+":
                case "-":
                case "*":
                case "/":
                case "%":
                    // int 'op' int => int
                    // int / int => number
                    if (left.canInt() && right.canInt()) {
                        if (operator == "/")
                            result.setType(TYPES.NUMBER);
                        else
                            result.setType(TYPES.INT);
                    }
                    // int 'op' number => number
                    else if (left.canInt() && right.isNumber() || right.canInt() && left.isNumber()) {
                        result.setType(TYPES.NUMBER);
                    }
                    // number 'op' number => number
                    else if (left.isNumber() && right.isNumber()) {
                        result.setType(TYPES.NUMBER);
                        // int 'op' null => int
                    }
                    else if (left.isInt() && right.isNull() || right.isInt() && left.isNull()) {
                        result.setType(TYPES.INT);
                    }
                    // number 'op' null => number
                    else if ((left.isNumber() && right.isNull()) || (right.isNumber() && left.isNull())) {
                        result.setType(TYPES.NUMBER);
                    }
                    else {
                        // NaN
                        result.setInvalid(generateErrorInformation(node, "NotANumberError"));
                    }
                    break;
                case "===":
                case "!==":
                    result.setType(TYPES.BOOLEAN);
                    if (left.isUndefined() || right.isUndefined()) {
                        var value = left.isUndefined() && right.isUndefined();
                        result.setStaticValue(operator == "===" ? value : !value);
                        return;
                    }
                    break;
                case "==": // comparison
                case "!=":
                case ">":
                case "<":
                case ">=":
                case "<=":
                    result.setType(TYPES.BOOLEAN);
                    if (left.isUndefined() || right.isUndefined()) {
                        var value = left.isUndefined() && right.isUndefined();
                        result.setStaticValue(operator == "!=" ? !value : value);
                        return;
                    }
                    break;
                default:
                    throw new Error("Operator not supported: " + operator);
            }
             if (left.hasStaticValue() && right.hasStaticValue()) {
                //console.log(left.getStaticValue(), operator, right.getStaticValue());
                result.setStaticValue(evaluator.getStaticValue(node));
            } else {
                result.setDynamicValue();
            }
        },

        AssignmentExpression: function (node, parent, context) {
            var right = context.getTypeInfo(node.right),
                result = ANNO(node);

            result.copy(right);
            result.setDynamicValue();

            // Check, if a assigned variable still has the same type as
            // before and update type of uninitialized variables.
            if (node.left.type == Syntax.Identifier && !context.inDeclaration() && right.isValid()) {
                var name = node.left.name;
                var scope = context.getScope();
                scope.updateTypeInfo(name, right);
            }
        },

        MemberExpression: function (node, parent, context) {
            var resultType = context.getTypeInfo(node),
                objectAnnotation = ANNO(node.object),
                propertyAnnotation = ANNO(node.property),
                scope = context.getScope();

            if(!objectAnnotation.isValid()) {
                resultType.setInvalid();
                return;
            }

            //console.log("Member", node.object.name, node.property.name);
            if (node.computed) {
                if (objectAnnotation.isArray()) {
                    // Property is computed, thus it could be a variable
                    var propertyType =  context.getTypeInfo(node.property);
                    if (!propertyType.canInt()) {
                        Shade.throwError(node, "Expected 'int' type for array accessor");
                    }
                    var elementInfo = objectAnnotation.getArrayElementType();
                    resultType.setType(elementInfo.type, elementInfo.kind);
                    return;
                }
                else {
                    resultType.setInvalid(generateErrorInformation(node, "Cannot access member via computed value from object", objectAnnotation.getTypeString()));

                    //Shade.throwError(node, "TypeError: Cannot access member via computed value from object '" + objectAnnotation.getTypeString());
                }
            }
            var propertyName = node.property.name;

            var objectOfInterest = common.getObjectReferenceFromNode(node.object, scope);

            objectOfInterest || Shade.throwError(node,"ReferenceError: " + node.object.name + " is not defined. Context: " + scope.str());

            if (!objectOfInterest.isValid() || objectOfInterest.getType() == TYPES.UNDEFINED) {  // e.g. var a = undefined; a.unknown;
                resultType.setInvalid(generateErrorInformation(node, "TypeError: Cannot read property '" + propertyName + "' of undefined"));
                return;
            }
            if (objectOfInterest.getType() != TYPES.OBJECT) { // e.g. var a = 5; a.unknown;
                resultType.setType(TYPES.UNDEFINED);
                return;
            }

            var objectInfo = scope.getObjectInfoFor(objectOfInterest);
            if(!objectInfo)
                Shade.throwError(node, "Internal: Incomplete registration for object: " + objectOfInterest.getTypeString() + ", " + JSON.stringify(node.object));

            objectAnnotation.copy(objectOfInterest);
            if (!objectInfo.hasOwnProperty(propertyName)) {
                resultType.setType(TYPES.UNDEFINED);
                propertyAnnotation.setType(TYPES.UNDEFINED);
                return;
            }

            var propertyTypeInfo = objectInfo[propertyName];
            propertyAnnotation.setFromExtra(propertyTypeInfo);
            resultType.setFromExtra(propertyTypeInfo);
        },

        CallExpression: function (node, parent, context) {
            var result = ANNO(node),
                scope = context.getScope(),
                args = context.getTypeInfo(node.arguments),
                extra, staticValue;

            if (!args.every(function (arg) {return arg.isValid() })) {
                result.setInvalid(generateErrorInformation(node, "Not all arguments types of call expression could be evaluated"));
                return;
            }

            // Call on an object, e.g. Math.cos()
            if (node.callee.type == Syntax.MemberExpression) {

                var memberExpression = context.getTypeInfo(node.callee);
                if(!memberExpression.isValid()) {
                    result.setInvalid();
                    return;
                }

                var object = node.callee.object,
                    propertyName = node.callee.property.name;

                var objectReference = context.getTypeInfo(object);
                if(!objectReference)  {
                    Shade.throwError(node, "Internal: No object info for: " + object);
                }
                var objectInfo = scope.getObjectInfoFor(objectReference);
                if(!objectInfo) { // Every object needs an info, otherwise we did something wrong
                    Shade.throwError(node, "Internal Error: No object registered for: " + objectReference.getTypeString() + JSON.stringify(node.object));
                }

                if (!memberExpression.isFunction()) { // e.g. Math.PI()
                    result.setInvalid();
                    if (objectInfo.hasOwnProperty(propertyName)) {
                      result.setError(generateErrorInformation(node, "TypeError: Property '" + propertyName + "' of object #<"+ objectReference.getTypeString() +"> is not a function"));
                    } else {
                      result.setError(generateErrorInformation(node, "TypeError: " + (object.type == Syntax.ThisExpression ? "'this'" : objectReference.getTypeString())+ " has no method '"+ propertyName + "'"));
                    }
                    return;
                }

                if (!objectInfo.hasOwnProperty(propertyName)) {
                    result.setType(TYPES.UNDEFINED);
                    return;
                }
                var propertyHandler = objectInfo[propertyName];

                if (typeof propertyHandler.evaluate != "function") {
                    Shade.throwError(node, "Internal: no handler registered for '" + propertyName + "'");
                }
                // Evaluate type of call

                try {
                    extra = propertyHandler.evaluate(result, args, scope, objectReference, context);
                    result.setFromExtra(extra);
                } catch (e) {
                    result.setInvalid(generateErrorInformation(node, e.message));
                    return;
                }

                // If we have a type, evaluate static value
                if (typeof propertyHandler.computeStaticValue != "function") {
                    debug && console.warn("No static evaluation exists for function", codegen.generate(node));
                    return;
                }
                staticValue = propertyHandler.computeStaticValue(result, args, scope, objectReference, context);
                if (staticValue !== undefined) {
                    result.setStaticValue(staticValue);
                }
                return;

            }  else if (node.callee.type == Syntax.Identifier) {
                var functionName = node.callee.name;
                var func = scope.getBindingByName(functionName);
                if (!func) {
                    Shade.throwError(node, "ReferenceError: " + functionName + " is not defined");
                }
                if(!func.isFunction()) {
                    Shade.throwError(node, "TypeError: " + func.getTypeString() + " is not a function");
                }
                try {
                    extra = context.callFunction(scope.getVariableIdentifier(functionName), args);
                    extra && result.setFromExtra(extra);
                } catch(e) {
                    result.setInvalid(generateErrorInformation(node, "Failure in function call: ", e.msg));
                }
                return;
            }

            throw new Error("Unhandled CallExpression:" + node.callee.type);

        },

        VariableDeclaration: function (node, parent, context) {
            context.setInDeclaration(false);
        },

        LogicalExpression: function (node, parent, context) {
            var left = context.getTypeInfo(node.left),
                right = context.getTypeInfo(node.right),
                result = ANNO(node);

            // static: true || false, dynamic: undefined
            var leftBool = left.getStaticTruthValue(),
                rightBool = right.getStaticTruthValue(),
                operator = node.operator;

            if (operator === "||") {
                if (leftBool === false) {
                    result.copy(right);
                    return;
                }
                if (leftBool === true) {
                    result.copy(left);
                    return;
                }
                // Left is dynamic, let's check right
                if (rightBool === false) {
                    // Now the result type is always the one of the left value
                    result.copy(left);
                    return;
                }
            } else if (operator === "&&") {
                if (leftBool === false) {
                    // T(x) == false => x && y == x
                    result.copy(left);
                    return;
                }
                if (leftBool === true) {
                    result.copy(right);
                    return;
                }
                // Left is dynamic, let's check right
                if (rightBool === true) {
                    // Now the result type is always the one of the left value
                    result.copy(left);
                    return;
                }
                if (rightBool === false) {
                    // Now the result must be false
                    result.setType(TYPES.BOOLEAN);
                    result.setStaticValue(false);
                    return;
                }
            }

            // If we can cast both sides to a common type, it's fine
            if(result.setCommonType(left, right)) {
                return;
            }

            result.setInvalid(generateErrorInformation(node, "Can't evaluate polymorphic logical expression"))

        },

        ConditionalExpression: function (node, parent, context) {
            var consequent = context.getTypeInfo(node.consequent),
                alternate = context.getTypeInfo(node.alternate),
                test = context.getTypeInfo(node.test),
                result = ANNO(node);

            var testResult = test.getStaticTruthValue();
            if(testResult === true) {
                result.copy(consequent);
            } else if (testResult === false) {
                result.copy(alternate);
            } else if (!result.setCommonType(consequent, alternate)) {
                result.setInvalid(generateErrorInformation(node, "Can't evaluate polymorphic conditional expression"))
            }

        }

    };

    ns.annotateRight  = function(ast) {

        if(!ast)
            throw Error("No node to analyze");

        var controller = new estraverse.Controller();
        var context = this;

        controller.traverse(ast, {
            enter: function(node) {
                if(node.type == Syntax.VariableDeclaration) {
                    context.setInDeclaration(true);
                }
            },
            leave: function(node, parent) {
                if (handlers.hasOwnProperty(node.type)) {
                    return handlers[node.type].call(this, node, parent, context);
                }
                return null;
            }
        });

    }
}(exports));
<|endoftext|>


<commit_before>const express = require('express');
const router = express.Router();
const path = require('path');
const passport = require('passport');

router.route('/')
  .get(passport.authenticate('google', {
      scope: ['profile']
  }));

router.route('/return')
  .get(passport.authenticate('google', {
      successRedirect: '/',
      failureRedirect: '/landing'
  }));

module.exports = router;
<commit_msg>adds email scope to google strategy<commit_after>const express = require('express');
const router = express.Router();
const path = require('path');
const passport = require('passport');

router.route('/')
  .get(passport.authenticate('google', {
      scope: ['profile', 'email']
  }));

router.route('/return')
  .get(passport.authenticate('google', {
      successRedirect: '/',
      failureRedirect: '/landing',
  }));

module.exports = router;
<|endoftext|>


<commit_before>var hasOwnProperty = Object.prototype.hasOwnProperty;

function isReactElement(value) {
  if (!hasOwnProperty.call(value, '$$typeof')) {
    return false;
  }

  if (typeof Symbol === 'function' && Symbol.keyFor) {
    return Symbol.keyFor(value.$$typeof) === 'react.element';
  } else {
    return value.$$typeof === 0xeac7;
  }
}

function stringifyArray(value, str, limit) {
  str += '[ ';

  for (var i = 0; i < value.length; i++) {
    if (i !== 0) {
      str += ', ';
    }

    str = stringify(value[i], str, limit);

    if (str.length > limit) {
      return str;
    }
  }

  return str + ' ]';
}

function stringifyReactElement(value, str) {
  var type = value.type;
  var name;

  str += '<';

  if (typeof type === 'function') {
    name = type.displayName || type.name;
  } else if (typeof type === 'string') {
    name = type;
  }

  str += name || 'Unknown';

  return str + ' />';
}

function stringifyObject(value, str, limit) {
  var shouldAddComma = false;

  str += '{ ';

  for (var prop in value) {
    if (hasOwnProperty.call(value, prop)) {
      if (shouldAddComma) {
        str += ', ';
      }

      str += prop + ': ';

      str = stringify(value[prop], str, limit);

      if (str.length > limit) {
        return str;
      }

      shouldAddComma = true;
    }
  }

  return str + ' }';
}

function stringifyFunction(value, str) {
  str += 'function';

  if (value.name) {
    str += ' ' + value.name.trim();
  }

  return str + '(){}';
}

function stringify(value, str, limit) {
  if (value === null) {
    return str + 'null';
  }

  if (Array.isArray(value)) {
    return stringifyArray(value, str, limit);
  }

  if (typeof value === 'object') {
    if (isReactElement(value)) {
      return stringifyReactElement(value, str, limit);
    } else {
      return stringifyObject(value, str, limit);
    }
  }

  if (typeof value === 'string') {
    return str + "'" + value + "'";
  }

  if (typeof value === 'function') {
    return stringifyFunction(value, str);
  }

  return str + String(value);
}

function sliceText(text, limit) {
  if (text.length > limit) {
    return text.slice(0, limit) + '…';
  } else {
    return text;
  }
}

module.exports = function stringifyWithLimit(value, limit) {
  return sliceText(stringify(value, '', limit), limit);
};

module.exports.sliceText = sliceText;
<commit_msg>fix eslint warning<commit_after>/* global Symbol */
var hasOwnProperty = Object.prototype.hasOwnProperty;

function isReactElement(value) {
  if (!hasOwnProperty.call(value, '$$typeof')) {
    return false;
  }

  if (typeof Symbol === 'function' && Symbol.keyFor) {
    return Symbol.keyFor(value.$$typeof) === 'react.element';
  } else {
    return value.$$typeof === 0xeac7;
  }
}

function stringifyArray(value, str, limit) {
  str += '[ ';

  for (var i = 0; i < value.length; i++) {
    if (i !== 0) {
      str += ', ';
    }

    str = stringify(value[i], str, limit);

    if (str.length > limit) {
      return str;
    }
  }

  return str + ' ]';
}

function stringifyReactElement(value, str) {
  var type = value.type;
  var name;

  str += '<';

  if (typeof type === 'function') {
    name = type.displayName || type.name;
  } else if (typeof type === 'string') {
    name = type;
  }

  str += name || 'Unknown';

  return str + ' />';
}

function stringifyObject(value, str, limit) {
  var shouldAddComma = false;

  str += '{ ';

  for (var prop in value) {
    if (hasOwnProperty.call(value, prop)) {
      if (shouldAddComma) {
        str += ', ';
      }

      str += prop + ': ';

      str = stringify(value[prop], str, limit);

      if (str.length > limit) {
        return str;
      }

      shouldAddComma = true;
    }
  }

  return str + ' }';
}

function stringifyFunction(value, str) {
  str += 'function';

  if (value.name) {
    str += ' ' + value.name.trim();
  }

  return str + '(){}';
}

function stringify(value, str, limit) {
  if (value === null) {
    return str + 'null';
  }

  if (Array.isArray(value)) {
    return stringifyArray(value, str, limit);
  }

  if (typeof value === 'object') {
    if (isReactElement(value)) {
      return stringifyReactElement(value, str, limit);
    } else {
      return stringifyObject(value, str, limit);
    }
  }

  if (typeof value === 'string') {
    return str + "'" + value + "'";
  }

  if (typeof value === 'function') {
    return stringifyFunction(value, str);
  }

  return str + String(value);
}

function sliceText(text, limit) {
  if (text.length > limit) {
    return text.slice(0, limit) + '…';
  } else {
    return text;
  }
}

module.exports = function stringifyWithLimit(value, limit) {
  return sliceText(stringify(value, '', limit), limit);
};

module.exports.sliceText = sliceText;
<|endoftext|>


<commit_before>var Text = require('../models/Text'),
    exec = require('child_process').exec;

/**
 * POST text
 */

exports.create = function(req, res) {
  var text = new Text({
    text: req.body.text,
    start_time: req.body.start_time,
    end_time: req.body.end_time,
    sentiment: req.body.DEBUG === 'TRUE' ? req.body.sentiment : null,
    sentiment_type: null
  });
  text.save(function(err, text) {
    if (err) {
      res.send(500, {
        "error": err
      });
    } else {
      res.send(200, { "id": text.id });
      text.fetchSentiment();
    }
  });
};

exports.happyTexts = function(req, res) {
  Text
    .where('sentiment_type').equals('positive')
    .where('sentiment').gt(0.1).exec(function(err, texts) {
    res.send(200, {
      "happyTexts": texts
    });
  });
};

exports.allTexts = function(req, res) {
  Text.find({}).exec(function(err, texts) {
    res.send(200, {
      "happyTexts": texts
    });
  });
};<commit_msg>standardize texts<commit_after>var Text = require('../models/Text'),
    exec = require('child_process').exec;

/**
 * POST text
 */

exports.create = function(req, res) {
  var text = new Text({
    text: req.body.text,
    start_time: req.body.start_time,
    end_time: req.body.end_time,
    sentiment: req.body.DEBUG === 'TRUE' ? req.body.sentiment : null,
    sentiment_type: null
  });
  text.save(function(err, text) {
    if (err) {
      res.send(500, {
        "error": err
      });
    } else {
      res.send(200, { "id": text.id });
      text.fetchSentiment();
    }
  });
};

exports.happyTexts = function(req, res) {
  Text
    .where('sentiment_type').equals('positive')
    .where('sentiment').gt(0.1).exec(function(err, texts) {
    res.send(200, {
      "texts": texts
    });
  });
};

exports.allTexts = function(req, res) {
  Text.find({}).exec(function(err, texts) {
    res.send(200, {
      "texts": texts
    });
  });
};<|endoftext|>


<commit_before>export {
  default
} from 'https://www.lactame.com//lib/molecular-formula/0.42.3/molecular-formula.js';
// } from 'http://localhost:9898/cheminfo-js/molecular-formula/dist/molecular-formula.js';
<commit_msg>chore: update molecular-formula to 0.42.4<commit_after>export {
  default
} from 'https://www.lactame.com//lib/molecular-formula/0.42.4/molecular-formula.js';
// } from 'http://localhost:9898/cheminfo-js/molecular-formula/dist/molecular-formula.js';
<|endoftext|>


<commit_before>"use strict";

const url = require("url");
const readline = require("readline");
const fs = require("fs-extra-promise");
const moment = require("moment");
const { SshClient } = require("ssh");
const { serviceMgr } = require("service");
const { asyncWithTmo } = require("misc");
const log = require("log");
const { AsyncEventEmitter } = require("emitter");
const GerritEventEmitter = require("./gerrit_event_emitter");

const DEFAULT_GERRIT_PORT = 29418;
const DEFAULT_GERRIT_TIMEOUT = 60 * 1000;

/*
Note that localhost below needs to be changed to the host where
gerrit is running!

To get started with gerrit docker container:
1. Start container
2. Navigate to http://localhost:8080
3. Upload the public key configured for your gerrit backend to the admin user
  a. Click Become in Top-right corner
  b. Click Administrator
  c. Go to Settings (Located under user-menu in top-right corner)
  d. Click SSH Publick keys
  e. Click Add Key...
  f. Paste content of public key and click Add
4. Create repo via CodeRepo from UI or REST api

To get started with client side:
1. To clone repo, create user that will clone in gerrit
  cat /home/$USER/.ssh/id_rsa.pub | ssh -p 29418 admin@localhost gerrit create-account --full-name "'Marcus Christensson'" --email "'marcus.christensson@combitech.se'" --ssh-key - $USER
2. Clone using
  git clone ssh://$USER@localhost:29418/REPO_NAME
3. Copy gerrit hooks to local repo
  scp -p -P 29418 $USER@localhost:hooks/commit-msg REPO_ROOT/.git/hooks/
4. Set name and email in cloned repo
  git config --local user.name "Marcus Christensson"
  git config --local user.email "marcus.christensson@combitech.se"
4. When pusing changes, push with
  git push origin HEAD:refs/for/master
5. Add rights for registered users to code-review +2 and submit from gerrit UI
  a. Go to Project -> List and select Project (Repository)
  b. Select Access tab and click Edit
  c. Click "Add Reference"
  d. Reference field shall be "refs/heads/*"
  e. Click "Add Permission..." and select "Label Code Review"
  f. Enter Group Name "Registered Users"
  g. Click "Add Permission..." and select "Submit"
  h. Enter Group Name "Registered Users"
  i. Click "Save Changes"
6. Code-review +2 and submit via gerrit UI or using:
  ssh -p 29418 $USER@localhost gerrit review `git rev-parse HEAD` --code-review '+2' --submit
*/

class GerritBackend extends AsyncEventEmitter {
    constructor(id, backend, Repository, Revision) {
        super();
        this.id = id;
        this.backend = backend;
        this.locks = {};
        this.Repository = Repository;
        this.Revision = Revision;
        this.gerritEmitter = new GerritEventEmitter();

        this.__ssh = new SshClient();
    }

    async start() {
        const info = url.parse(this.backend.uri);
        const privateKey = await fs.readFileAsync(this.backend.privateKeyPath);
        const params = {
            host: info.hostname,
            port: info.port || DEFAULT_GERRIT_PORT,
            username: info.auth || process.env.USER,
            privateKey: privateKey
        };

        try {
            // TODO: Handle errors correctly from ssh client!
            await this.__ssh.connect(params, (err) => {
                throw new Error(`SSH client error: ${err}`);
            });
            await this._startMonitorEventStream();
        } catch (err) {
            log.error(`Failed to connect to gerrit using uri ${this.backend.uri}`, err);
        }
    }

    async validateRepository(/* event, data */) {
        // TODO: Validate gerrit specific options
    }

    async _readStream(stream) {
        const lines = [];
        const rl = readline.createInterface({
            input: stream,
            terminal: false
        });

        await new Promise((resolve) => {
            rl.on("line", (line) => {
                if (line.length === 0) {
                    return;
                }

                lines.push(line);
            });

            rl.on("close", resolve);
        });

        return lines;
    }

    async _execGerritCommand(cmd) {
        log.verbose("gerrit command:", cmd);

        return this.__ssh.execute(`gerrit ${cmd}`);
    }

    async _execGerritCommandReadOutput(cmd) {
        return new Promise(async (resolve) => {
            const { stdout, stderr } = await this._execGerritCommand(cmd);
            const exitCodePromise = new Promise((resolve) => stdout.on("close", resolve));
            const outLines = await this._readStream(stdout, "stdout");
            const errLines = await this._readStream(stderr, "stderr");

            if (errLines.length > 0) {
                log.error(`gerrit command: ${cmd} produced error output, stderr:`, errLines.join("\n"));
            }
            if (outLines.length > 0) {
                log.verbose(`gerrit command: ${cmd} produced output, stdout:`, outLines.join("\n"));
            }
            const exitCode = await exitCodePromise;
            resolve({ exitCode, outLines, errLines });
        });
    }

    async _onPatchsetCreated(event) {
        const changeId = event.changeKey.id;
        const repositoryId = event.project;
        const repository = await this.Repository.findOne({ _id: repositoryId });
        if (repository) {
            const revision = await this.Revision.findOne({ _id: changeId });
            const refIndex = revision ? revision.patches.length + 1 : 1;
            // TODO: Shall we use email and name from uploader?
            // TODO: Handle draft changes
            const ref = {
                index: refIndex,
                email: event.uploader.email,
                name: event.uploader.name,
                submitted: moment.unix(event.patchSet.createdOn).utc().format(),
                comment: event.change.commitMessage,
                change: {
                    oldrev: event.patchSet.parents[0],
                    newrev: event.patchSet.revision,
                    refname: event.patchSet.ref // Use event.refName all the time instead?
                }
            };

            await this.Revision.allocate(repository._id, changeId, ref);
            log.info(`Gerrit event allocated revision ${changeId}`);
        }
    }

    async _onChangeMerged(event) {
        const changeId = event.changeKey.id;
        // Make sure that we know about repo
        const repositoryId = event.project;
        const repository = await this.Repository.findOne({ _id: repositoryId });
        if (repository) {
            // And that we know about revision...
            const revision = await this.Revision.findOne({ _id: changeId });
            if (revision) {
                const ref = {
                    index: revision.patches.length + 1,
                    email: event.patchSet.uploader.email,
                    name: event.patchSet.uploader.name,
                    submitted: moment.unix(event.patchSet.createdOn).utc().format(),
                    comment: event.change.commitMessage,
                    change: {
                        oldrev: event.patchSet.parents[0],
                        newrev: event.newRev,
                        refname: event.refName
                    }
                };
                await revision.setMerged(ref);
                await this.emit("revision.merged", revision);
                log.info(`Gerrit event merged revision ${changeId}`);
            }
        }
    }

    async _startMonitorEventStream() {
        const { stdout, stderr } = await this._execGerritCommand("stream-events");

        this.gerritEmitter.addListener("stderr-output", (line) => {
            log.error("Gerrit event stream stderr: ", line);
        });
        this.gerritEmitter.addListener("patchset-created", this._onPatchsetCreated.bind(this));
        this.gerritEmitter.addListener("change-merged", this._onChangeMerged.bind(this));

        await this.gerritEmitter.start(stdout, stderr);
    }

    async create(repository) {
        log.info(`gerrit create repo ${repository._id}`);
        const { exitCode, errLines } = await this._execGerritCommandReadOutput(
            `create-project ${repository._id} ` +
            "--submit-type REBASE_IF_NECESSARY --empty-commit"
        );

        if (exitCode !== 0) {
            if (errLines[0].match(/Project already exists/)) {
                // OK, project already exists!
                log.info(`gerrit repo ${repository._id} already exists, attached to existing`);
            } else {
                throw new Error(`Gerrit command failed with exit code ${exitCode}. stderr=${errLines.join("\n")}`);
            }
        }
    }

    async merge(repository, revision) {
        log.info(`gerrit merge ${revision._id} in repo ${repository._id}`);
        // ssh -p 29418 $USER@localhost gerrit review `git rev-parse HEAD` --code-review '+2' --submit
        const ref = revision.patches[revision.patches.length - 1];
        const { exitCode, errLines } = await this._execGerritCommandReadOutput(
            `review ${ref.change.newrev} --submit`
        );

        if (exitCode !== 0) {
            const needsCodeReviewLines = errLines.filter((line) => line.match(/needs Code-Review/));
            if (needsCodeReviewLines.length > 0) {
                throw new Error(`Cannot merge, needs Code-Review: ${needsCodeReviewLines[0]}`);
            } else {
                throw new Error(`Gerrit command failed with exit code ${exitCode}. stderr=${errLines.join("\n")}`);
            }
        }

        let revisionMergedListener;
        const revisionMergedPromise = new Promise((resolve) => {
            revisionMergedListener = (resolve, mergedRev) => {
                if (mergedRev._id === revision._id) {
                    resolve(mergedRev);
                }
            };
            revisionMergedListener = revisionMergedListener.bind(this, resolve);
            this.on("revision.merged", revisionMergedListener);
        });
        await asyncWithTmo(
            revisionMergedPromise,
            DEFAULT_GERRIT_TIMEOUT,
            new Error(`Timeout while waiting for merge of revision ${revision._id} to complete`)
        );
        if (revisionMergedListener) {
            this.removeListener("revision.merged", revisionMergedListener);
        }

        return false;
    }

    async getUri(backend, repository) {
        const gerritUrl = url.parse(backend.uri);
        const connectUri = gerritUrl.format().replace(gerritUrl.auth, "$USER");

        return `${connectUri}/${repository._id}`;
    }

    async update(/* repository */) {
        // TODO: Implement update
    }

    async remove(/* repository */) {
        // TODO: Implement remove
    }

    async _getUserPublicKeys(username) {
        const restClient = await serviceMgr.use("userrepo");

        return await restClient.get(`/user/${username}/keys`);
    }

    async dispose() {
        this.removeAllListeners();
        this.gerritEmitter.removeAllListeners();
        await this.gerritEmitter.dispose();
        await this.__ssh.disconnect();
    }
}

module.exports = GerritBackend;
<commit_msg>CodeRepo: Fix race condition at gerrit backend merge operation<commit_after>"use strict";

const url = require("url");
const readline = require("readline");
const fs = require("fs-extra-promise");
const moment = require("moment");
const { SshClient } = require("ssh");
const { serviceMgr } = require("service");
const { asyncWithTmo } = require("misc");
const log = require("log");
const { AsyncEventEmitter } = require("emitter");
const GerritEventEmitter = require("./gerrit_event_emitter");

const DEFAULT_GERRIT_PORT = 29418;
const DEFAULT_GERRIT_TIMEOUT = 60 * 1000;

/*
Note that localhost below needs to be changed to the host where
gerrit is running!

To get started with gerrit docker container:
1. Start container
2. Navigate to http://localhost:8080
3. Upload the public key configured for your gerrit backend to the admin user
  a. Click Become in Top-right corner
  b. Click Administrator
  c. Go to Settings (Located under user-menu in top-right corner)
  d. Click SSH Publick keys
  e. Click Add Key...
  f. Paste content of public key and click Add
4. Create repo via CodeRepo from UI or REST api

To get started with client side:
1. To clone repo, create user that will clone in gerrit
  cat /home/$USER/.ssh/id_rsa.pub | ssh -p 29418 admin@localhost gerrit create-account --full-name "'Marcus Christensson'" --email "'marcus.christensson@combitech.se'" --ssh-key - $USER
2. Clone using
  git clone ssh://$USER@localhost:29418/REPO_NAME
3. Copy gerrit hooks to local repo
  scp -p -P 29418 $USER@localhost:hooks/commit-msg REPO_ROOT/.git/hooks/
4. Set name and email in cloned repo
  git config --local user.name "Marcus Christensson"
  git config --local user.email "marcus.christensson@combitech.se"
4. When pusing changes, push with
  git push origin HEAD:refs/for/master
5. Add rights for registered users to code-review +2 and submit from gerrit UI
  a. Go to Project -> List and select Project (Repository)
  b. Select Access tab and click Edit
  c. Click "Add Reference"
  d. Reference field shall be "refs/heads/*"
  e. Click "Add Permission..." and select "Label Code Review"
  f. Enter Group Name "Registered Users"
  g. Click "Add Permission..." and select "Submit"
  h. Enter Group Name "Registered Users"
  i. Click "Save Changes"
6. Code-review +2 and submit via gerrit UI or using:
  ssh -p 29418 $USER@localhost gerrit review `git rev-parse HEAD` --code-review '+2' --submit
*/

class GerritBackend extends AsyncEventEmitter {
    constructor(id, backend, Repository, Revision) {
        super();
        this.id = id;
        this.backend = backend;
        this.locks = {};
        this.Repository = Repository;
        this.Revision = Revision;
        this.gerritEmitter = new GerritEventEmitter();

        this.__ssh = new SshClient();
    }

    async start() {
        const info = url.parse(this.backend.uri);
        const privateKey = await fs.readFileAsync(this.backend.privateKeyPath);
        const params = {
            host: info.hostname,
            port: info.port || DEFAULT_GERRIT_PORT,
            username: info.auth || process.env.USER,
            privateKey: privateKey
        };

        try {
            // TODO: Handle errors correctly from ssh client!
            await this.__ssh.connect(params, (err) => {
                throw new Error(`SSH client error: ${err}`);
            });
            await this._startMonitorEventStream();
        } catch (err) {
            log.error(`Failed to connect to gerrit using uri ${this.backend.uri}`, err);
        }
    }

    async validateRepository(/* event, data */) {
        // TODO: Validate gerrit specific options
    }

    async _readStream(stream) {
        const lines = [];
        const rl = readline.createInterface({
            input: stream,
            terminal: false
        });

        await new Promise((resolve) => {
            rl.on("line", (line) => {
                if (line.length === 0) {
                    return;
                }

                lines.push(line);
            });

            rl.on("close", resolve);
        });

        return lines;
    }

    async _execGerritCommand(cmd) {
        log.verbose("gerrit command:", cmd);

        return this.__ssh.execute(`gerrit ${cmd}`);
    }

    async _execGerritCommandReadOutput(cmd) {
        return new Promise(async (resolve) => {
            const { stdout, stderr } = await this._execGerritCommand(cmd);
            const exitCodePromise = new Promise((resolve) => stdout.on("close", resolve));
            const outLines = await this._readStream(stdout, "stdout");
            const errLines = await this._readStream(stderr, "stderr");

            if (errLines.length > 0) {
                log.error(`gerrit command: ${cmd} produced error output, stderr:`, errLines.join("\n"));
            }
            if (outLines.length > 0) {
                log.verbose(`gerrit command: ${cmd} produced output, stdout:`, outLines.join("\n"));
            }
            const exitCode = await exitCodePromise;
            resolve({ exitCode, outLines, errLines });
        });
    }

    async _onPatchsetCreated(event) {
        const changeId = event.changeKey.id;
        const repositoryId = event.project;
        const repository = await this.Repository.findOne({ _id: repositoryId });
        if (repository) {
            const revision = await this.Revision.findOne({ _id: changeId });
            const refIndex = revision ? revision.patches.length + 1 : 1;
            // TODO: Shall we use email and name from uploader?
            // TODO: Handle draft changes
            const ref = {
                index: refIndex,
                email: event.uploader.email,
                name: event.uploader.name,
                submitted: moment.unix(event.patchSet.createdOn).utc().format(),
                comment: event.change.commitMessage,
                change: {
                    oldrev: event.patchSet.parents[0],
                    newrev: event.patchSet.revision,
                    refname: event.patchSet.ref // Use event.refName all the time instead?
                }
            };

            await this.Revision.allocate(repository._id, changeId, ref);
            log.info(`Gerrit event allocated revision ${changeId}`);
        }
    }

    async _onChangeMerged(event) {
        const changeId = event.changeKey.id;
        // Make sure that we know about repo
        const repositoryId = event.project;
        const repository = await this.Repository.findOne({ _id: repositoryId });
        if (repository) {
            // And that we know about revision...
            const revision = await this.Revision.findOne({ _id: changeId });
            if (revision) {
                const ref = {
                    index: revision.patches.length + 1,
                    email: event.patchSet.uploader.email,
                    name: event.patchSet.uploader.name,
                    submitted: moment.unix(event.patchSet.createdOn).utc().format(),
                    comment: event.change.commitMessage,
                    change: {
                        oldrev: event.patchSet.parents[0],
                        newrev: event.newRev,
                        refname: event.refName
                    }
                };
                await revision.setMerged(ref);
                await this.emit("revision.merged", revision);
                log.info(`Gerrit event merged revision ${changeId}`);
            }
        }
    }

    async _startMonitorEventStream() {
        const { stdout, stderr } = await this._execGerritCommand("stream-events");

        this.gerritEmitter.addListener("stderr-output", (line) => {
            log.error("Gerrit event stream stderr: ", line);
        });
        this.gerritEmitter.addListener("patchset-created", this._onPatchsetCreated.bind(this));
        this.gerritEmitter.addListener("change-merged", this._onChangeMerged.bind(this));

        await this.gerritEmitter.start(stdout, stderr);
    }

    async create(repository) {
        log.info(`gerrit create repo ${repository._id}`);
        const { exitCode, errLines } = await this._execGerritCommandReadOutput(
            `create-project ${repository._id} ` +
            "--submit-type REBASE_IF_NECESSARY --empty-commit"
        );

        if (exitCode !== 0) {
            if (errLines[0].match(/Project already exists/)) {
                // OK, project already exists!
                log.info(`gerrit repo ${repository._id} already exists, attached to existing`);
            } else {
                throw new Error(`Gerrit command failed with exit code ${exitCode}. stderr=${errLines.join("\n")}`);
            }
        }
    }

    async merge(repository, revision) {
        log.info(`gerrit merge ${revision._id} in repo ${repository._id}`);
        // ssh -p 29418 $USER@localhost gerrit review `git rev-parse HEAD` --code-review '+2' --submit

        let revisionMergedListener;
        const revisionMergedPromise = new Promise((resolve) => {
            revisionMergedListener = (resolve, mergedRev) => {
                if (mergedRev._id === revision._id) {
                    resolve(mergedRev);
                }
            };
            revisionMergedListener = revisionMergedListener.bind(this, resolve);
            this.on("revision.merged", revisionMergedListener);
        });

        try {
            const ref = revision.patches[revision.patches.length - 1];
            const { exitCode, errLines } = await this._execGerritCommandReadOutput(
                `review ${ref.change.newrev} --submit`
            );

            if (exitCode !== 0) {
                const needsCodeReviewLines = errLines.filter((line) => line.match(/needs Code-Review/));
                if (needsCodeReviewLines.length > 0) {
                    throw new Error(`Cannot merge, needs Code-Review: ${needsCodeReviewLines[0]}`);
                } else {
                    throw new Error(`Gerrit command failed with exit code ${exitCode}. stderr=${errLines.join("\n")}`);
                }
            }

            await asyncWithTmo(
                revisionMergedPromise,
                DEFAULT_GERRIT_TIMEOUT,
                new Error(`Timeout while waiting for merge of revision ${revision._id} to complete`)
            );
        } catch (error) {
            throw error;
        } finally {
            if (revisionMergedListener) {
                this.removeListener("revision.merged", revisionMergedListener);
            }
        }

        return false;
    }

    async getUri(backend, repository) {
        const gerritUrl = url.parse(backend.uri);
        const connectUri = gerritUrl.format().replace(gerritUrl.auth, "$USER");

        return `${connectUri}/${repository._id}`;
    }

    async update(/* repository */) {
        // TODO: Implement update
    }

    async remove(/* repository */) {
        // TODO: Implement remove
    }

    async _getUserPublicKeys(username) {
        const restClient = await serviceMgr.use("userrepo");

        return await restClient.get(`/user/${username}/keys`);
    }

    async dispose() {
        this.removeAllListeners();
        this.gerritEmitter.removeAllListeners();
        await this.gerritEmitter.dispose();
        await this.__ssh.disconnect();
    }
}

module.exports = GerritBackend;
<|endoftext|>


<commit_before>// this class is not really related to a sampleToc but can be used for any TOC

import API from 'src/util/api';
import Versioning from 'src/util/versioning';
import Color from 'src/util/color';

const SpectraConfigs = {
  IR: {
    tocFilter: (entry) => entry.value.nbIR && !entry.value.hidden,
    tocCallback: (entry) => {
      entry.value.nbSpectra = entry.value.nbIR;
    },
    getSpectra: (sample) => {
      if (
        sample &&
        sample.$content &&
        sample.$content.spectra &&
        Array.isArray(sample.$content.spectra.ir)
      ) {
        let spectra = sample.$content.spectra.ir;
        return spectra;
      } else {
        return [];
      }
    },
    chartPrefs: {
      yLabel: 'Absorbance',
      displayYAxis: [
        'display',
        'main',
        'sec'
      ],
      xLabel: 'Wavelength [cm-1]',
      displayXAxis: [
        'display',
        'flip',
        'main',
        'sec'
      ]
    }
  },
  Raman: {
    tocFilter: (entry) => entry.value.nbRaman && !entry.value.hidden,
    tocCallback: (entry) => {
      entry.value.nbSpectra = entry.value.nbRaman;
    },
    getSpectra: (sample) => {
      if (
        sample &&
        sample.$content &&
        sample.$content.spectra &&
        Array.isArray(sample.$content.spectra.raman)
      ) {
        let spectra = sample.$content.spectra.raman;
        return spectra;
      } else {
        return [];
      }
    },
    chartPrefs: {
      yLabel: 'Absorbance',
      displayYAxis: [
        'display',
        'main',
        'sec'
      ],
      xLabel: 'Wavelength [cm-1]',
      displayXAxis: [
        'display',
        'flip',
        'main',
        'sec'
      ]
    }
  },
  '1H NMR': {
    tocFilter: (entry) => entry.value.nb1h && !entry.value.hidden,
    tocCallback: (entry) => {
      entry.value.nbSpectra = entry.value.nb1h;
    },
    getSpectra: (sample) => {
      if (
        sample &&
        sample.$content &&
        sample.$content.spectra &&
        Array.isArray(sample.$content.spectra.nmr)
      ) {
        let spectra = sample.$content.spectra.nmr;
        spectra = spectra.filter(
          (spectrum) => spectrum.dimension === 1 && spectrum.nucleus[0] === '1H'
        );
        spectra.forEach((spectrum) => {
          let info = [];
          if (spectrum.nucleus) info.push(spectrum.nucleus[0]);
          if (spectrum.experiment) info.push(spectrum.experiment);
          if (spectrum.solvent) info.push(spectrum.solvent);
          if (spectrum.frequency) info.push(spectrum.frequency.toFixed(0));
          spectrum.info = info.join(', ');
        });
        return spectra;
      } else {
        return [];
      }
    },
    chartPrefs: {
      yLabel: 'Intensity',
      displayYAxis: [
        'main',
        'sec'
      ],
      xLabel: 'δ [ppm]',
      displayXAxis: [
        'display',
        'flip',
        'main',
        'sec'
      ]
    },

  },
  '13C NMR': {
    tocFilter: (entry) => entry.value.nb13c && !entry.value.hidden,
    tocCallback: (entry) => {
      entry.value.nbSpectra = entry.value.nb1h;
    },
    getSpectra: (sample) => {
      if (
        sample &&
        sample.$content &&
        sample.$content.spectra &&
        Array.isArray(sample.$content.spectra.nmr)
      ) {
        let spectra = sample.$content.spectra.nmr;
        spectra = spectra.filter(
          (spectrum) => spectrum.dimension === 1 && spectrum.nucleus[0] === '13C'
        );
        spectra.forEach((spectrum) => {
          let info = [];
          if (spectrum.nucleus) info.push(spectrum.nucleus[0]);
          if (spectrum.experiment) info.push(spectrum.experiment);
          if (spectrum.solvent) info.push(spectrum.solvent);
          if (spectrum.frequency) info.push(spectrum.frequency.toFixed(0));
          spectrum.info = info.join(', ');
        });
        return spectra;
      } else {
        return [];
      }
    },
    chartPrefs: {
      yLabel: 'Intensity',
      displayYAxis: [
        'main',
        'sec'
      ],
      xLabel: 'δ [ppm]',
      displayXAxis: [
        'display',
        'flip',
        'main',
        'sec'
      ]
    }
  },
  Chromatography: {
    tocFilter: (entry) => entry.value.mbChrom && !entry.value.hidden,
    tocCallback: (entry) => {
      entry.value.nbSpectra = entry.value.mbChrom;
    },
    getSpectra: (sample) => {
      if (
        sample &&
        sample.$content &&
        sample.$content.spectra &&
        Array.isArray(sample.$content.spectra.chromatogram)
      ) {
        let spectra = sample.$content.spectra.chromatogram;
        return spectra;
      } else {
        return [];
      }
    },
    chartPrefs: {
      yLabel: 'Intensity',
      displayYAxis: [
        'main',
        'sec'
      ],
      xLabel: 'Time [s]',
      displayXAxis: [
        'display',
        'flip',
        'main',
        'sec'
      ]
    }
  }
};

class SpectraDataSet {
  constructor(roc, sampleToc, options = {}) {
    this.roc = roc;
    this.sampleToc = sampleToc;
    this.spectraConfig = undefined;
    this.defaultAttributes = options.defaultAttributes || {};
  }

  getChartPrefs() {
    return this.spectraConfig.chartPrefs;
  }

  /**
   * @param {object} [options={}]
   * @param {string} [options.varName='analysisKind'] contains the name of the variable containing the form value
   * @param {string} [options.schemaVarName='analysisKindSchema'] contains the name of the variable containing the form schema
   * @return {string} the form to select group}
   */
  async initializeAnalysis(options = {}) {
    const {
      schemaVarName = 'analysisKindSchema',
      varName = 'analysisKind',
      cookieName = 'eln-default-analysis-kind'
    } = options;

    var possibleAnalysis = Object.keys(SpectraConfigs);
    var defaultAnalysis = localStorage.getItem(cookieName);
    if (possibleAnalysis.indexOf(defaultAnalysis) === -1) {
      defaultAnalysis = possibleAnalysis[0];
    }
    var schema = {
      type: 'object',
      properties: {
        analysis: {
          type: 'string',
          enum: possibleAnalysis,
          default: defaultAnalysis,
          required: true
        }
      }
    };

    API.createData(schemaVarName, schema);

    let analysisKind = await API.createData(varName, {
      analysis: defaultAnalysis
    });

    this.spectraConfig = SpectraConfigs[defaultAnalysis];

    await this.refresh();

    let mainData = Versioning.getData();
    mainData.onChange((evt) => {
      if (evt.jpath[0] === varName) {
        localStorage.setItem(cookieName, analysisKind.analysis);
        this.spectraConfig = SpectraConfigs[String(analysisKind.analysis)];
        this.refresh();
      }
    });

    return analysisKind;
  }

  refresh() {
    if (!this.sampleToc) return;
    this.sampleToc.options.filter = this.spectraConfig.tocFilter;
    this.sampleToc.options.callback = this.spectraConfig.tocCallback;
    this.sampleToc.refresh();
  }

  async processAction(action) {
    console.log({ action });
    switch (action.name) {
      case 'clickedSample':
        this.clickedSample(action.value);
        break;
      case 'refresh':
        this.refresh();
        break;
      case 'hideSpectra':
        this.hideSpectra();
        break;
      case 'hideAllSpectra':
        this.hideAllSpectra();
        break;
      case 'showOnlySpectra':
        this.showOnlySpectra();
        break;
      case 'forceRecolor': {
        let selectedSpectra = API.getData('selectedSpectra');
        selectedSpectra.forEach((spectrum) => spectrum.color = '');
        recolor(selectedSpectra);
        selectedSpectra.triggerChange();
        break;
      }
      case 'showSpectra':
        this.showSpectra();
        break;
      case 'showAllSpectra':
        this.showAllSpectra();
        break;
      case 'clearSelectedSamples':
        {
          let selectedSpectra = API.getData('selectedSpectra');
          selectedSpectra.length = 0;
          selectedSpectra.triggerChange();
        }
        break;
      case 'addSelectedSamples':
        await this.addSelectedSamples(API.getData('tocSelected').resurrect());
        break;
      case 'addSample':
        await this.addSelectedSamples([action.value.resurrect()]);
        break;
      case 'addSpectrum':
        await this.addSpectrum(
          API.getData('tocClicked').resurrect(),
          action.value.resurrect()
        );
        break;
      default:
    }
  }

  async clickedSample(samples) {
    if (samples.length !== 1) {
      API.createData('spectra', []);
      return;
    }
    let uuid = String(samples[0].id);
    let data = await this.roc.document(uuid, { varName: 'linkedSample' });
    let spectra = this.spectraConfig.getSpectra(data);
    API.createData('spectra', spectra);
  }

  showAllSpectra() {
    let selectedSpectra = API.getData('selectedSpectra');
    for (let spectrum of selectedSpectra) {
      spectrum.display = true;
    }
    API.getData('selectedSpectra').triggerChange();
  }

  hideAllSpectra() {
    let selectedSpectra = API.getData('selectedSpectra');
    for (let spectrum of selectedSpectra) {
      spectrum.display = false;
    }
    API.getData('selectedSpectra').triggerChange();
  }

  showSpectra() {
    let selectedSpectra = API.getData('selectedSpectra');
    let currentlySelectedSpectra = API.getData('currentlySelectedSpectra');
    for (let currentlySelectedSpectrum of currentlySelectedSpectra) {
      let spectrum = selectedSpectra.filter((spectrum) => String(spectrum.id) === String(currentlySelectedSpectrum.id))[0];
      spectrum.display = true;
    }
    API.getData('selectedSpectra').triggerChange();
  }

  showOnlySpectra() {
    let selectedSpectra = API.getData('selectedSpectra');
    for (let spectrum of selectedSpectra) {
      spectrum.display = false;
    }
    let currentlySelectedSpectra = API.getData('currentlySelectedSpectra');
    for (let currentlySelectedSpectrum of currentlySelectedSpectra) {
      let spectrum = selectedSpectra.filter((spectrum) => String(spectrum.id) === String(currentlySelectedSpectrum.id))[0];
      spectrum.display = true;
    }
    API.getData('selectedSpectra').triggerChange();
  }

  hideSpectra() {
    let selectedSpectra = API.getData('selectedSpectra');
    let currentlySelectedSpectra = API.getData('currentlySelectedSpectra');
    for (let currentlySelectedSpectrum of currentlySelectedSpectra) {
      let spectrum = selectedSpectra.filter((spectrum) => String(spectrum.id) === String(currentlySelectedSpectrum.id))[0];
      spectrum.display = false;
    }
    API.getData('selectedSpectra').triggerChange();
  }

  addSpectrum(tocEntry, spectrum) {
    let selectedSpectra = API.getData('selectedSpectra');
    this.addSpectrumToSelected(spectrum, tocEntry, selectedSpectra);
    recolor(selectedSpectra);
    selectedSpectra.triggerChange();
  }

  async addSelectedSamples(tocSelected) {
    let selectedSpectra = API.getData('selectedSpectra');
    // count the number of sampleIDs to determine the number of colros
    let promises = [];
    for (let tocEntry of tocSelected) {
      promises.push(
        this.roc.document(tocEntry.id).then((sample) => {
          let spectra = this.spectraConfig.getSpectra(sample);
          for (let spectrum of spectra) {
            if (spectrum.jcamp && spectrum.jcamp.filename) {
              this.addSpectrumToSelected(spectrum, tocEntry, selectedSpectra);
            }
          }
        })
      );
    }
    await Promise.all(promises);
    recolor(selectedSpectra);
    selectedSpectra.triggerChange();
  }

  addSpectrumToSelected(spectrum, tocEntry, selectedSpectra) {
    if (spectrum.jcamp) {
      let spectrumID = String(`${tocEntry.value.reference} / ${spectrum.jcamp.filename.replace(/.*\/(.*)\..*/, '$1')}`);
      let sampleID = String(tocEntry.id);
      if (selectedSpectra.filter((spectrum) => String(spectrum.id) === spectrumID).length > 0) return;
      spectrum.sampleID = sampleID;
      spectrum.id = spectrumID;
      spectrum.display = true;
      for (let key in this.defaultAttributes) {
        spectrum[key] = this.defaultAttributes[key];
      }

      spectrum.deconvolutionReference = false;
      spectrum.sampleCode = tocEntry.key.slice(1).join('_');
      spectrum.toc = tocEntry;
      spectrum.category = spectrum.sampleCode;
      spectrum._highlight = spectrumID;
      selectedSpectra.push(spectrum);
    }
  }
}

function recolor(selectedSpectra) {
  // need to count the categories
  let categoryColors = {};
  let existingColors = 0;
  for (let spectrum of selectedSpectra) {
    let category = String(spectrum.category);
    if (categoryColors[category] === undefined) {
      if (spectrum.color) {
        categoryColors[String(spectrum.category)] = spectrum.color;
        existingColors++;
      } else {
        categoryColors[String(spectrum.category)] = '';
      }
    }
  }

  let nbColors = Math.max(
    8,
    1 << Math.ceil(Math.log2(Object.keys(categoryColors).length))
  );
  const colors = Color.getDistinctColorsAsString(nbColors);
  let i = existingColors;
  for (let key in categoryColors) {
    if (!categoryColors[key]) {
      categoryColors[key] = colors[i++];
    }
  }
  for (let spectrum of selectedSpectra) {
    if (!spectrum.color) {
      spectrum.color = categoryColors[String(spectrum.category)];
    }
  }
}

module.exports = SpectraDataSet;
<commit_msg>chore: fix space<commit_after>// this class is not really related to a sampleToc but can be used for any TOC

import API from 'src/util/api';
import Versioning from 'src/util/versioning';
import Color from 'src/util/color';

const SpectraConfigs = {
  IR: {
    tocFilter: (entry) => entry.value.nbIR && !entry.value.hidden,
    tocCallback: (entry) => {
      entry.value.nbSpectra = entry.value.nbIR;
    },
    getSpectra: (sample) => {
      if (
        sample &&
        sample.$content &&
        sample.$content.spectra &&
        Array.isArray(sample.$content.spectra.ir)
      ) {
        let spectra = sample.$content.spectra.ir;
        return spectra;
      } else {
        return [];
      }
    },
    chartPrefs: {
      yLabel: 'Absorbance',
      displayYAxis: [
        'display',
        'main',
        'sec'
      ],
      xLabel: 'Wavelength [cm-1]',
      displayXAxis: [
        'display',
        'flip',
        'main',
        'sec'
      ]
    }
  },
  Raman: {
    tocFilter: (entry) => entry.value.nbRaman && !entry.value.hidden,
    tocCallback: (entry) => {
      entry.value.nbSpectra = entry.value.nbRaman;
    },
    getSpectra: (sample) => {
      if (
        sample &&
        sample.$content &&
        sample.$content.spectra &&
        Array.isArray(sample.$content.spectra.raman)
      ) {
        let spectra = sample.$content.spectra.raman;
        return spectra;
      } else {
        return [];
      }
    },
    chartPrefs: {
      yLabel: 'Absorbance',
      displayYAxis: [
        'display',
        'main',
        'sec'
      ],
      xLabel: 'Wavelength [cm-1]',
      displayXAxis: [
        'display',
        'flip',
        'main',
        'sec'
      ]
    }
  },
  '1H NMR': {
    tocFilter: (entry) => entry.value.nb1h && !entry.value.hidden,
    tocCallback: (entry) => {
      entry.value.nbSpectra = entry.value.nb1h;
    },
    getSpectra: (sample) => {
      if (
        sample &&
        sample.$content &&
        sample.$content.spectra &&
        Array.isArray(sample.$content.spectra.nmr)
      ) {
        let spectra = sample.$content.spectra.nmr;
        spectra = spectra.filter(
          (spectrum) => spectrum.dimension === 1 && spectrum.nucleus[0] === '1H'
        );
        spectra.forEach((spectrum) => {
          let info = [];
          if (spectrum.nucleus) info.push(spectrum.nucleus[0]);
          if (spectrum.experiment) info.push(spectrum.experiment);
          if (spectrum.solvent) info.push(spectrum.solvent);
          if (spectrum.frequency) info.push(spectrum.frequency.toFixed(0));
          spectrum.info = info.join(', ');
        });
        return spectra;
      } else {
        return [];
      }
    },
    chartPrefs: {
      yLabel: 'Intensity',
      displayYAxis: [
        'main',
        'sec'
      ],
      xLabel: 'δ [ppm]',
      displayXAxis: [
        'display',
        'flip',
        'main',
        'sec'
      ]
    },

  },
  '13C NMR': {
    tocFilter: (entry) => entry.value.nb13c && !entry.value.hidden,
    tocCallback: (entry) => {
      entry.value.nbSpectra = entry.value.nb1h;
    },
    getSpectra: (sample) => {
      if (
        sample &&
        sample.$content &&
        sample.$content.spectra &&
        Array.isArray(sample.$content.spectra.nmr)
      ) {
        let spectra = sample.$content.spectra.nmr;
        spectra = spectra.filter(
          (spectrum) => spectrum.dimension === 1 && spectrum.nucleus[0] === '13C'
        );
        spectra.forEach((spectrum) => {
          let info = [];
          if (spectrum.nucleus) info.push(spectrum.nucleus[0]);
          if (spectrum.experiment) info.push(spectrum.experiment);
          if (spectrum.solvent) info.push(spectrum.solvent);
          if (spectrum.frequency) info.push(spectrum.frequency.toFixed(0));
          spectrum.info = info.join(', ');
        });
        return spectra;
      } else {
        return [];
      }
    },
    chartPrefs: {
      yLabel: 'Intensity',
      displayYAxis: [
        'main',
        'sec'
      ],
      xLabel: 'δ [ppm]',
      displayXAxis: [
        'display',
        'flip',
        'main',
        'sec'
      ]
    }
  },
  Chromatography: {
    tocFilter: (entry) => entry.value.mbChrom && !entry.value.hidden,
    tocCallback: (entry) => {
      entry.value.nbSpectra = entry.value.mbChrom;
    },
    getSpectra: (sample) => {
      if (
        sample &&
        sample.$content &&
        sample.$content.spectra &&
        Array.isArray(sample.$content.spectra.chromatogram)
      ) {
        let spectra = sample.$content.spectra.chromatogram;
        return spectra;
      } else {
        return [];
      }
    },
    chartPrefs: {
      yLabel: 'Intensity',
      displayYAxis: [
        'main',
        'sec'
      ],
      xLabel: 'Time [s]',
      displayXAxis: [
        'display',
        'flip',
        'main',
        'sec'
      ]
    }
  }
};

class SpectraDataSet {
  constructor(roc, sampleToc, options = {}) {
    this.roc = roc;
    this.sampleToc = sampleToc;
    this.spectraConfig = undefined;
    this.defaultAttributes = options.defaultAttributes || {};
  }

  getChartPrefs() {
    return this.spectraConfig.chartPrefs;
  }

  /**
   * @param {object} [options={}]
   * @param {string} [options.varName='analysisKind'] contains the name of the variable containing the form value
   * @param {string} [options.schemaVarName='analysisKindSchema'] contains the name of the variable containing the form schema
   * @return {string} the form to select group}
   */
  async initializeAnalysis(options = {}) {
    const {
      schemaVarName = 'analysisKindSchema',
      varName = 'analysisKind',
      cookieName = 'eln-default-analysis-kind'
    } = options;

    var possibleAnalysis = Object.keys(SpectraConfigs);
    var defaultAnalysis = localStorage.getItem(cookieName);
    if (possibleAnalysis.indexOf(defaultAnalysis) === -1) {
      defaultAnalysis = possibleAnalysis[0];
    }
    var schema = {
      type: 'object',
      properties: {
        analysis: {
          type: 'string',
          enum: possibleAnalysis,
          default: defaultAnalysis,
          required: true
        }
      }
    };

    API.createData(schemaVarName, schema);

    let analysisKind = await API.createData(varName, {
      analysis: defaultAnalysis
    });

    this.spectraConfig = SpectraConfigs[defaultAnalysis];

    await this.refresh();

    let mainData = Versioning.getData();
    mainData.onChange((evt) => {
      if (evt.jpath[0] === varName) {
        localStorage.setItem(cookieName, analysisKind.analysis);
        this.spectraConfig = SpectraConfigs[String(analysisKind.analysis)];
        this.refresh();
      }
    });

    return analysisKind;
  }

  refresh() {
    if (!this.sampleToc) return;
    this.sampleToc.options.filter = this.spectraConfig.tocFilter;
    this.sampleToc.options.callback = this.spectraConfig.tocCallback;
    this.sampleToc.refresh();
  }

  async processAction(action) {
    console.log({ action });
    switch (action.name) {
      case 'clickedSample':
        this.clickedSample(action.value);
        break;
      case 'refresh':
        this.refresh();
        break;
      case 'hideSpectra':
        this.hideSpectra();
        break;
      case 'hideAllSpectra':
        this.hideAllSpectra();
        break;
      case 'showOnlySpectra':
        this.showOnlySpectra();
        break;
      case 'forceRecolor': {
        let selectedSpectra = API.getData('selectedSpectra');
        selectedSpectra.forEach((spectrum) => spectrum.color = '');
        recolor(selectedSpectra);
        selectedSpectra.triggerChange();
        break;
      }
      case 'showSpectra':
        this.showSpectra();
        break;
      case 'showAllSpectra':
        this.showAllSpectra();
        break;
      case 'clearSelectedSamples':
        {
          let selectedSpectra = API.getData('selectedSpectra');
          selectedSpectra.length = 0;
          selectedSpectra.triggerChange();
        }
        break;
      case 'addSelectedSamples':
        await this.addSelectedSamples(API.getData('tocSelected').resurrect());
        break;
      case 'addSample':
        await this.addSelectedSamples([action.value.resurrect()]);
        break;
      case 'addSpectrum':
        await this.addSpectrum(
          API.getData('tocClicked').resurrect(),
          action.value.resurrect()
        );
        break;
      default:
    }
  }

  async clickedSample(samples) {
    if (samples.length !== 1) {
      API.createData('spectra', []);
      return;
    }
    let uuid = String(samples[0].id);
    let data = await this.roc.document(uuid, { varName: 'linkedSample' });
    let spectra = this.spectraConfig.getSpectra(data);
    API.createData('spectra', spectra);
  }

  showAllSpectra() {
    let selectedSpectra = API.getData('selectedSpectra');
    for (let spectrum of selectedSpectra) {
      spectrum.display = true;
    }
    API.getData('selectedSpectra').triggerChange();
  }

  hideAllSpectra() {
    let selectedSpectra = API.getData('selectedSpectra');
    for (let spectrum of selectedSpectra) {
      spectrum.display = false;
    }
    API.getData('selectedSpectra').triggerChange();
  }

  showSpectra() {
    let selectedSpectra = API.getData('selectedSpectra');
    let currentlySelectedSpectra = API.getData('currentlySelectedSpectra');
    for (let currentlySelectedSpectrum of currentlySelectedSpectra) {
      let spectrum = selectedSpectra.filter((spectrum) => String(spectrum.id) === String(currentlySelectedSpectrum.id))[0];
      spectrum.display = true;
    }
    API.getData('selectedSpectra').triggerChange();
  }

  showOnlySpectra() {
    let selectedSpectra = API.getData('selectedSpectra');
    for (let spectrum of selectedSpectra) {
      spectrum.display = false;
    }
    let currentlySelectedSpectra = API.getData('currentlySelectedSpectra');
    for (let currentlySelectedSpectrum of currentlySelectedSpectra) {
      let spectrum = selectedSpectra.filter((spectrum) => String(spectrum.id) === String(currentlySelectedSpectrum.id))[0];
      spectrum.display = true;
    }
    API.getData('selectedSpectra').triggerChange();
  }

  hideSpectra() {
    let selectedSpectra = API.getData('selectedSpectra');
    let currentlySelectedSpectra = API.getData('currentlySelectedSpectra');
    for (let currentlySelectedSpectrum of currentlySelectedSpectra) {
      let spectrum = selectedSpectra.filter((spectrum) => String(spectrum.id) === String(currentlySelectedSpectrum.id))[0];
      spectrum.display = false;
    }
    API.getData('selectedSpectra').triggerChange();
  }

  addSpectrum(tocEntry, spectrum) {
    let selectedSpectra = API.getData('selectedSpectra');
    this.addSpectrumToSelected(spectrum, tocEntry, selectedSpectra);
    recolor(selectedSpectra);
    selectedSpectra.triggerChange();
  }

  async addSelectedSamples(tocSelected) {
    let selectedSpectra = API.getData('selectedSpectra');
    // count the number of sampleIDs to determine the number of colros
    let promises = [];
    for (let tocEntry of tocSelected) {
      promises.push(
        this.roc.document(tocEntry.id).then((sample) => {
          let spectra = this.spectraConfig.getSpectra(sample);
          for (let spectrum of spectra) {
            if (spectrum.jcamp && spectrum.jcamp.filename) {
              this.addSpectrumToSelected(spectrum, tocEntry, selectedSpectra);
            }
          }
        })
      );
    }
    await Promise.all(promises);
    recolor(selectedSpectra);
    selectedSpectra.triggerChange();
  }

  addSpectrumToSelected(spectrum, tocEntry, selectedSpectra) {
    if (spectrum.jcamp) {
      let spectrumID = String(`${tocEntry.value.reference} / ${spectrum.jcamp.filename.replace(/.*\/(.*)\..*/, '$1')}`);
      let sampleID = String(tocEntry.id);
      if (selectedSpectra.filter((spectrum) => String(spectrum.id) === spectrumID).length > 0) return;
      spectrum.sampleID = sampleID;
      spectrum.id = spectrumID;
      spectrum.display = true;
      for (let key in this.defaultAttributes) {
        spectrum[key] = this.defaultAttributes[key];
      }

      spectrum.deconvolutionReference = false;
      spectrum.sampleCode = tocEntry.key.slice(1).join('_');
      spectrum.toc = tocEntry;
      spectrum.category = spectrum.sampleCode;
      spectrum._highlight = spectrumID;
      selectedSpectra.push(spectrum);
    }
  }
}

function recolor(selectedSpectra) {
  // need to count the categories
  let categoryColors = {};
  let existingColors = 0;
  for (let spectrum of selectedSpectra) {
    let category = String(spectrum.category);
    if (categoryColors[category] === undefined) {
      if (spectrum.color) {
        categoryColors[String(spectrum.category)] = spectrum.color;
        existingColors++;
      } else {
        categoryColors[String(spectrum.category)] = '';
      }
    }
  }

  let nbColors = Math.max(
    8,
    1 << Math.ceil(Math.log2(Object.keys(categoryColors).length))
  );
  const colors = Color.getDistinctColorsAsString(nbColors);
  let i = existingColors;
  for (let key in categoryColors) {
    if (!categoryColors[key]) {
      categoryColors[key] = colors[i++];
    }
  }
  for (let spectrum of selectedSpectra) {
    if (!spectrum.color) {
      spectrum.color = categoryColors[String(spectrum.category)];
    }
  }
}

module.exports = SpectraDataSet;
<|endoftext|>