/**
* @module Q
*/
var Q = require('../Q');
var fs = require('fs');
/**
* Creates a Q.Tree object
* @class Tree
* @namespace Q
* @constructor
* @param {object} [linked={}] If supplied, then this object is
* used as the internal tree that Tree operates on.
*/
module.exports = function (linked) {
if (linked === undefined) {
linked = {};
}
if (Q.typeOf(linked) === 'Q.Tree') {
linked = linked.getAll();
}
this.typename = "Q.Tree";
/**
* Loads data into a tree from a file.
* @method load
* @param {string} filename The filename of the file to load.
* @param {function} [callback=null] Function to call back, with params (err, data)
* @throws {Q.Exception} if filename is not string or array of strings
*/
this.load = function (filename, callback) {
var that = this;
var filenames;
switch (Q.typeOf(filename)) {
case 'string':
filenames = [filename];
break;
case 'array':
filenames = filename;
break;
default:
throw new Q.Exception("Q.Tree.load: filename has to be a string or array");
}
var p = new Q.Pipe(filenames, function (params) {
// All the files were loaded, time to merge them in the right order
for (var i=0; i<filenames.length; ++i) {
var k = filenames[i];
if (params[k][0]) {
that.merge(params[k][0]);
}
}
this.filename = filename;
callback && callback.call(that, null, that.getAll());
});
for (var i=0; i<filenames.length; ++i) {
(function (i) {
fs.readFile(filenames[i].replace('/', Q.DS), 'utf-8', function (err, data) {
if (err) {
callback && callback.call(that, err);
} else {
try {
data = data.replace(/\s*(?!<")\/\*[^\*]+\*\/(?!")\s*/gi, '');
data = JSON.parse(data);
} catch (e) {
callback && callback.call(that, e);
}
p.fill(filenames[i])(data);
}
});
})(i);
}
};
/**
* Saves a (sub)tree of parameters to a file
* @method save
* @param {string} filename The filename to save into.
* If tree was loaded from a single file, you can leave this blank to update that file.
* @param {array} [arrayPath=[]] Array of keys identifying the path of the subtree to save
* @param {array} [prefixPath=[]] Array of keys identifying the prefix path of the subtree to save
* @param {function} [callback=null] Function to call back, with params (err)
*/
this.save = function (filename, arrayPath, prefixPath, callback) {
if (!filename && (typeof this.filename === 'string')) {
filename = this.filename;
}
if (typeof arrayPath === 'function' || typeof arrayPath === 'undefined') {
callback = arrayPath;
arrayPath = prefixPath = [];
} else if (typeof prefixPath === 'function' || typeof prefixPath === 'undefined') {
callback = prefixPath;
prefixPath = [];
}
var d, data = this.get.apply(this, [arrayPath]), that = this;
if(Q.typeOf(prefixPath) !== 'array') prefixPath = prefixPath ? [prefixPath] : [];
for (var i=prefixPath.length-1; i>=0; i--) {
d = {};
d[prefixPath[i]] = data;
data = d;
}
to_save = JSON.stringify(data, null, '\t');
var mask = process.umask(parseInt(Q.Config.get(['Q', 'internal', 'umask'], "0000"), 8));
fs.writeFile(filename.replace('/', Q.DS), to_save, function (err) {
process.umask(mask);
callback && callback.call(that, err);
});
};
/**
* Gets the entire tree of parameters
* @method getAll
* @return {object}
*/
this.getAll = function () {
return linked;
};
/**
* Gets the value of a field in the tree
* @method get
* @param {string|array} [keys=[]] A key or an array of keys for traversing the tree.
* @param {mixed} [def=undefined] The value to return if the field is not found. Defaults to undefined.
* @return {mixed} The field if it is found, otherwise def or undefined.
* @throws {Q.Exception} if subtree is not an object
*/
this.get = function (keys, def) {
if (typeof keys === 'undefined') keys = [];
if (typeof keys === 'string') {
var arr = [];
for (var j=0; j<arguments.length-1; ++j) {
arr.push(arguments[j]);
}
keys = arr;
def = arguments[arguments.length-1];
}
var result = linked, key, sawKeys = [];
for (var i=0, len = keys.length; i<len; ++i) {
key = keys[i];
if (typeof result !== 'object') {
return def; // silently ignore the rest of the path
// var sawString = '["' + sawKeys.join('"]["') + '"]';
// throw new Q.Exception(
// "Q.Tree: subtree at '"+sawString+"' is not an object",
// {keys: keys, key: key}
// );
}
if (!result || key == null || !(key in result)) {
return def;
}
result = result[key];
sawKeys.push(key);
}
return result;
};
/**
* Sets the value of a field in the tree. If only one argument is given,
* it is assigned as tree value
* @method set
* @param {string|array} keys A key or an array of keys for traversing the tree.
* @param {mixed} value The value to set for that field.
* @return {Q.Tree} Returns itself for chaining
* @chainable
*/
this.set = function (keys, value) {
var k;
if (arguments.length === 1) {
linked = (typeof keys === "object") ? keys : [keys];
} else {
if (Q.typeOf(keys) === 'object') {
for (k in keys) {
linked[k] = keys[k];
}
} else {
if (typeof keys === 'string') {
var arr = [];
for (var j=0; j<arguments.length-1; ++j) {
arr.push(arguments[j]);
}
keys = arr;
value = arguments[arguments.length-1];
}
var result = linked, key;
for (var i=0, len = keys.length; i<len-1; ++i) {
key = keys[i];
if (!(key in result) || Q.typeOf(result[key]) !== 'object') {
result[key] = {}; // overwrite with an object
}
result = result[key];
}
if (key = keys[len-1]) {
result[key] = value;
}
}
}
return this;
};
/**
* Clears the value of a field, removing that key from the tree
* @method clear
* @param {string|array} [keys=null] A key or an array of keys for traversing the tree. If null, clears entire tree.
* @return {boolean} Returns whether the field to be cleared was found
*/
this.clear = function (keys) {
if (!keys) {
linked = {};
return;
}
if (typeof keys === 'string') {
keys = [keys];
}
var result = linked, key;
for (var i=0, len = keys.length; i<len; ++i) {
key = keys[i];
if (typeof result !== 'object') {
return false;
}
if (!(key in result)) {
return false;
}
if (i === len - 1) {
// return the final value
delete result[key];
return true;
}
result = result[key];
}
return false;
};
/**
* Traverse the tree depth-first and call the callback
* @method depthFirst
* @param {Function} callback Will receive (path, value, tree, context)
* @param {mixed} [context=null] To propagate some context to the callback
*/
this.depthFirst = function(callback, context) {
_depthFirst.call(this, [], linked, callback, context);
};
/**
* Traverse the tree breadth-first and call the callback
* @method breadthFirst
* @param {Function} callback Will receive (path, value, tree, context)
* @param {mixed} [context=null] To propagate some context to the callback
*/
this.breadthFirst = function(callback, context) {
callback.call(this, [], linked, linked, context);
_breadthFirst.call(this, [], linked, callback, context);
};
/**
* Calculates a diff between this tree and another tree
* @method diff
* @param {Q.Tree} tree
* @return {Q.Tree} This tree holds the results of the diff
*/
this.diff = function(tree) {
var context = {
from: this,
to: tree,
diff: new Q.Tree()
};
this.depthFirst(_diffTo, context);
tree.depthFirst(_diffFrom, context);
return context.diff;
};
function _diffTo (path, value, arr, context) {
var valueTo = context.to.get(path, null);
if ((!Q.isPlainObject(value) || !Q.isPlainObject(valueTo))
&& valueTo !== value) {
if (Q.isArrayLike(value) && Q.isArrayLike(valueTo)) {
valueTo = {replace: valueTo};
}
context.diff.set(path, valueTo);
}
if (valueTo == null) {
return false;
}
}
function _diffFrom (path, value, arr, context) {
var valueFrom = context.from.get(path, undefined);
if (valueFrom === undefined) {
context.diff.set(path, value);
return false;
}
}
/**
* Merges a tree over the top of an existing tree
* @method merge
* @param {Q.Tree|Object} second The Object or Q.Tree to merge over the existing tree.
* @param {boolean} [under=false] If true, merges the second under this tree, instead of over it.
* By default, second is merged on top of this tree.
* @return {object} Returns the resulting tree, modified by the merge.
**/
this.merge = function(second, under) {
if (Q.typeOf(second) === 'Q.Tree') {
this.merge(second.getAll(), under);
} else if (typeof second === 'object') {
if (under === true) {
linked = _merge(second, linked);
} else {
linked = _merge(linked, second);
}
} else {
return false;
}
return this;
};
function _merge(first, second) {
var result = (Q.typeOf(second) === 'object' ? {} : []);
var k;
// copy first to the result
if (Q.typeOf(first) === 'array' && second.replace) {
return second.replace;
}
for (k in first) {
result[k] = first[k];
}
switch (Q.typeOf(second)) {
case 'array':
// merge in values if they are not in array yet
// if array contains scalar values only unique values are kept
for (k=0; k<second.length; k++) {
if (result.indexOf(second[k]) < 0) {
result.push(second[k]);
}
}
break;
case 'object':
for (k in second) {
if (!(k in result)) {
// key is not in result so just add it
result[k] = second[k];
} else if (typeof result[k] !== 'object' || result[k] === null) {
// result[k] is scalar type
result[k] = second[k];
} else if (typeof second[k] !== 'object' || second[k] === null) {
// key already in result but second[k] is scalar
result[k] = second[k];
} else {
// otherwise second[k] is an object so merge it in
result[k] = _merge(result[k], second[k]);
}
}
break;
case 'null':
break;
default:
throw new Q.Exception("Cannot merge scalar '"+second+"' into Q.Tree");
}
return result;
}
};
function _depthFirst(subpath, obj, callback, context) {
var k, v, path;
for (k in obj) {
v = obj[k];
path = subpath.concat([k]);
if (false === callback.call(this, path, v, obj, context)) {
continue;
}
if (Q.isPlainObject(v)) {
_depthFirst.call(this, path, v, callback, context);
}
}
}
function _breadthFirst(subpath, obj, callback, context) {
var k, v, path;
for (k in obj) {
v = obj[k];
path = subpath.concat([k]);
if (false === callback.call(this, path, v, obj, context)) {
break;
}
}
for (k in obj) {
if (Q.isPlainObject(v)) {
path = subpath.concat([k]);
_breadthFirst.call(this, path, v, callback);
}
}
}