diffusion
Version:
Diffusion JavaScript client
359 lines (269 loc) • 8.57 kB
JavaScript
/*eslint valid-jsdoc: "off"*/
var requireNonNull = require('util/require-non-null');
var JSONPointer = require('data/json/json-pointer');
var EMPTY = new JSONPointerMap();
function JSONPointerMap() {
this.root = new Entry(null, JSONPointer.ROOT);
this.size = 0;
var self = this;
/**
* Add an entry.
* <P>
* By design, no constraints are applied to the supplied pointer. E.g. it is
* possible to add "x/foo" followed by "x/1", even though these
* can't be valid paths in a single document. This supports the structural
* diff use case where we might "add("x/foo", REMOVE)", then "add("x/1", INSERT)".
*
* @param pointer the key
* @param value may not be null
* @returns the previous value
*/
this.put = function(pointer, value) {
requireNonNull(value, "value");
var node = self.root;
var i = 0;
for (; i < pointer.segments.length;) {
var s = pointer.segments[i++];
var c = node.findChild(s);
if (c === null) {
node = node.addChild(s);
break;
}
node = c;
}
for (; i < pointer.segments.length; i++) {
node = node.addChild(pointer.segments[i]);
}
return node.setValue(value);
};
/**
* @returns {boolean} true if this map has a value for pointer
*/
this.contains = function(pointer) {
return self.get(pointer) !== null;
};
/**
* @returns {Object|Null} the element for pointer, or null if there is no value;
*/
this.get = function(pointer) {
var entry = self.getEntry(pointer);
if (entry) {
return entry.value;
}
return null;
};
/**
* Returns the entry for pointer. Entries reflect the tree structure, and might not have a value.
*
* @param pointer
* @returns {Entry|Null} the entry for pointer, or null if there is no entry
*/
this.getEntry = function(pointer) {
var result = self.root;
for (var i = 0; i < pointer.segments.length; ++i) {
result = result.findChild(pointer.segments[i]);
if (result === null) {
return null;
}
}
return result;
};
this.descendants = function(pointer) {
var result = new JSONPointerMap();
var thisNode = self.root;
var resultNode = result.root;
var segments = pointer.segments;
var length = segments.length;
if (length === 0) {
return this;
}
for (var i = 0; i < length - 1; ++i) {
var s = segments[i];
thisNode = thisNode.findChild(s);
if (thisNode === null) {
return EMPTY;
}
resultNode = resultNode.addChild(s);
}
var last = thisNode.findChild(segments[length - 1]);
if (last === null) {
return EMPTY;
}
resultNode.children.push(last);
result.size = last.count();
return result;
};
this.intersection = function(pointer) {
var result = new JSONPointerMap();
var thisNode = self.root;
var resultNode = result.root;
var segments = pointer.segments;
var length = segments.length;
if (length === 0) {
return this;
}
if (thisNode.value !== null) {
resultNode.setValue(thisNode.value);
}
for (var i = 0; i < length - 1; ++i) {
var s = segments[i];
thisNode = thisNode.findChild(s);
if (thisNode === null) {
return result;
}
resultNode = resultNode.addChild(s);
if (thisNode.value !== null) {
resultNode.setValue(thisNode.value);
}
}
var last = thisNode.findChild(segments[length - 1]);
if (last === null) {
return result;
}
resultNode.children.push(last);
result.size += last.count();
return result;
};
this.iterator = function() {
var stack = [self.root];
return new EntryIterator(function() {
for (;;) {
var r = stack.shift();
if (r === undefined) {
return null;
}
var n = r.children.length;
for (; n > 0; --n) {
stack.unshift(r.children[n - 1]);
}
if (r.value !== null) {
return r;
}
}
});
};
/**
* @returns a post-order iterator
*/
this.postOrder = function() {
var stack = [new PostOrderState(self.root)];
return new EntryIterator(function() {
for (;;) {
var r = stack[0];
if (r === undefined) {
return null;
}
var c = r.nextChild();
if (c === null) {
stack.shift();
if (r.node.value !== null) {
return r.node;
}
} else if (c.children.length === 0) {
return c;
} else {
stack.unshift(new PostOrderState(c));
}
}
});
};
this.toString = function() {
var parts = ['{'];
var iter = self.iterator();
while (iter.hasNext()) {
if (parts.length > 1) {
parts.push(', ');
}
parts.push(iter.next());
}
parts.push('}');
return parts.join('');
};
/**
* Returned through {@link JSONPointerMap#getEntry} and iterators.
* <p>
* Following a sequence of put operations, all entries will have a value, or
* children, or both. Through the {@link Entry#removeDescendants()} method,
* it is possible for an entry to have neither a value or children.
* @constructor
*/
function Entry(segment, pointer) {
this.segment = segment;
this.pointer = pointer;
this.children = [];
this.value = null;
this.count = function() {
var result = this.value ? 1 : 0;
for (var i = 0; i < this.children.length; ++i) {
result += this.children[i].count();
}
return result;
};
this.addChild = function(s) {
var c = new Entry(s, pointer.withSegment(s));
this.children.push(c);
return c;
};
this.findChild = function(s) {
for (var i = 0; i < this.children.length; ++i) {
if (s.equals(this.children[i].segment)) {
return this.children[i];
}
}
return null;
};
this.setValue = function(newValue) {
var previous = this.value;
if (previous === null) {
self.size++;
}
this.value = newValue;
return previous;
};
this.removeDescendants = function() {
var removed = 0;
for (var i = 0; i < this.children.length; ++i) {
removed += this.children[i].count();
}
this.children = [];
self.size -= removed;
return removed;
};
this.numberOfChildren = function() {
var result = 0;
for (var i = 0; i < this.children.length; ++i) {
result += (this.children[i].value ? 1 : 0);
}
return result;
};
this.toString = function() {
return pointer + "=" + this.value;
};
}
}
function PostOrderState(node) {
this.node = node;
var nextChild = 0;
this.nextChild = function() {
var children = node.children;
if (nextChild >= children.length) {
return null;
}
return children[nextChild++];
};
}
function EntryIterator(nextWithValue) {
var next = nextWithValue();
this.hasNext = function() {
return next !== null;
};
this.next = function() {
if (next === null) {
throw new Error("No such element");
}
var result = next;
next = nextWithValue();
return result;
};
}
module.exports = JSONPointerMap;