diff --git a/lib/common.coffee b/lib/common.coffee index 1834d4d..3243668 100644 --- a/lib/common.coffee +++ b/lib/common.coffee @@ -1,4 +1,22 @@ ### +# Copyright 2013-2016 Zaid Abdulla +# +# GenieACS is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# GenieACS is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with GenieACS. If not, see . +# +# This file incorporates work covered by the following copyright and +# permission notice: +# # Copyright 2013 Fanoos Telecom # # Permission is hereby granted, free of charge, to any person obtaining a copy @@ -122,6 +140,28 @@ exports.matchType = (src, dst) -> dst +pathOverlap = (a, b, start) -> + if a.length == b.length + res = 3 + else if a.length > b.length + res = 2 + else + res = 1 + + for i in [(start ? 0)...Math.min(a.length, b.length)] by 1 + if a[i]? + if not b[i]? + res &= 2 + else if a[i] != b[i] + return 0 + else if b[i]? + res &= 1 + + return 0 if not res + + return res + + exports.UNDEFINED_TYPE = UNDEFINED_TYPE exports.NULL_TYPE = NULL_TYPE exports.NUMBER_TYPE = NUMBER_TYPE @@ -131,3 +171,4 @@ exports.OBJECT_TYPE = OBJECT_TYPE exports.REGEXP_TYPE = REGEXP_TYPE exports.typeOf = typeOf +exports.pathOverlap = pathOverlap diff --git a/lib/device.coffee b/lib/device.coffee new file mode 100644 index 0000000..6dfeeed --- /dev/null +++ b/lib/device.coffee @@ -0,0 +1,370 @@ +### +# Copyright 2013-2016 Zaid Abdulla +# +# GenieACS is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# GenieACS is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with GenieACS. If not, see . +### + +common = require './common' + + +sanitizeDeclaration = (declaration, wildcard, now) -> + if typeof declaration is 'number' + declaration = [declaration] + + if declaration[7]? + declaration[7] = sanitizeParameterValue(declaration[7]) + + for i in [0...declaration.length] by 2 when declaration[i]? + if declaration[i] <= 0 + declaration[i] = now + declaration[i] + else + declaration[i] = Math.min(now, declaration[i]) + + return declaration + + +sanitizeProperties = (properties, wildcard) -> + if not wildcard + if typeof properties is 'number' + properties = [properties] + else if properties[1] + properties[1] = 1 + + for i in [2...properties.length] by 2 \ + when properties[i]? and properties[i + 1]? + properties[0] = Math.max(properties[0] ? 0, properties[i]) + + if properties[6]? and properties[7]? and not (properties[6] < properties[2]) + properties[2] = properties[6] + properties[3] = 0 + else if properties[2]? and properties[3] and not (properties[2] < properties[6]) + properties[6] = properties[2] + properties[7] = null + + else if typeof properties isnt 'number' + properties = properties[0] + + return properties + + +sanitizeParameterValue = (parameterValue) -> + if parameterValue[0]? + switch parameterValue[1] + when 'xsd:boolean' + if typeof parameterValue[0] isnt 'boolean' + parameterValue = parameterValue.slice() + parameterValue[0] = !!JSON.parse(parameterValue[0]) + when 'xsd:string' + if typeof parameterValue[0] isnt 'string' + parameterValue = parameterValue.slice() + parameterValue[0] = '' + parameterValue[0] + when 'xsd:int', 'xsd:unsignedInt' + if typeof parameterValue[0] isnt 'number' + parameterValue = parameterValue.slice() + parameterValue[0] = +parameterValue[0] + when 'xsd:dateTime' + if typeof parameterValue[0] isnt 'number' + parameterValue = parameterValue.slice() + if parameterValue[0].getTime? + parameterValue[0] = parameterValue[0].getTime() + else if isNaN(parameterValue[0]) + parameterValue[0] = Date.parse(parameterValue[0]) + else + parameterValue[0] = +parameterValue[0] + else + if parameterValue[1]? + throw new Error("Parameter value type \"#{parameterValue[1]}\" not recognized.") + + # Ensure consistency after serialization + parameterValue[0] = JSON.parse(JSON.stringify(parameter[0])) + + return parameterValue + + +init = () -> + return [[[null, [0, 1]]]] + + +declare = (deviceData, path, declaration, now) -> + wildcard = false + for i in [0...path.length] when not path[i]? + wildcard = true + break + + declaration = sanitizeDeclaration(declaration, wildcard, now) + + ref = deviceData + depth = 0 + for i in [0...path.length] by 1 + if not (p = path[i])? + ++ depth + continue + + ref[1 + depth] ?= {} + ref[1 + depth][p] ?= [] + ref = ref[1 + depth][p] + depth = 0 + + ref[0] ?= [] + ref[0][depth] ?= [] + + if wildcard + ref[0][depth][0] = Math.max(ref[0][depth][0] ? 0, declaration[0]) + else if not ref[0][depth][0]? + ref[0][depth][0] = declaration + else + for i in [0...declaration.length] by 2 + if declaration[i]? + ref[0][depth][0][i] = Math.max(ref[0][depth][0][i] ? 0, declaration[i]) + + if declaration[i + 1]? + ref[0][depth][0][i + 1] = declaration[i + 1] + + return + + +set = (deviceData, path, revision, properties) -> + ++ revision # revisions start at layer 1 + wildcard = false + for i in [0...path.length] when not path[i]? + wildcard = true + break + + properties = sanitizeProperties(properties, wildcard) + + ref = deviceData + depth = 0 + for i in [0...path.length] by 1 + if not (p = path[i])? + ++ depth + continue + + if not wildcard and properties[1] + # Update existing timestamp for ancestors + ref[0] ?= [] + ref[0][0] ?= [] + ref[0][0][revision] ?= [] + if not (ref[0][0][revision][0] >= properties[0]) + ref[0][0][revision][0] = properties[0] + ref[0][0][revision][1] = 1 + + if not (ref[0][0][revision][2] >= properties[0]) + ref[0][0][revision][2] = properties[0] + ref[0][0][revision][3] = 1 + + ref[1 + depth] ?= {} + ref[1 + depth][p] ?= [] + ref = ref[1 + depth][p] + depth = 0 + + ref[0] ?= [] + ref[0][depth] ?= [] + + if wildcard + ref[0][depth][revision] = Math.max(ref[0][depth][revision] ? 0, properties) + else if not ref[0][0][revision]? + ref[0][0][revision] = properties + else + for i in [0...properties.length] by 2 \ + when properties[i]?# and properties[i + 1]? + if not (properties[i] < ref[0][0][revision][i]) + ref[0][0][revision][i] = properties[i] + ref[0][0][revision][i + 1] = properties[i + 1] + + if properties[7]? + ref[0][0][revision][7] = sanitizeParameterValue(ref[0][0][revision][7]) + + return + + +traverse = (deviceData, pattern, revision, callback) -> + if not pattern? + pattern = [] + pattern.length = 99 + + if not revision? + revision = 99 + + ++ revision # First layer is for declarations + + collectTimestamps = (ref, path, root, timestamps, wildcard) -> + # Skip properties of parameter we're currently at + skip = if root == path.length then 1 else 0 + + if ref[0]? + for i in [skip...ref[0].length] by 1 + if ref[0][i]? and i <= pattern.length - path.length + p = path.slice() + p.length += i + if wildcard or i > 0 + revisions = ref[0][i] + else + # Extract only timestamp from properties array + revisions = (j?[0] for j in ref[0][i]) + + current = -1 + for j in [Math.min(revision, revisions.length - 1)..1] by -1 when revisions[j]? + current = revisions[j]; break + + timestamps.push([p, root, revisions, current, current]) + + for i in [skip...ref.length - 1] by 1 + if ref[1 + i]? and i < pattern.length - path.length + p = path.slice() + p.length += i + for k, v of ref[1+i] + if not pattern[path.length + i]? or pattern[path.length + i] == k + collectTimestamps(v, p.concat(k), root, timestamps, wildcard or i > 0) + + return + + processOverlaps = (timestamps, root, skip) -> + skip = 1 if not skip + for i in [skip...timestamps.length] by 1 + for j in [0...i] by 1 + if timestamps[i][0].length > timestamps[j][0].length + t = timestamps[j] + tt = timestamps[i] + else + t = timestamps[i] + tt = timestamps[j] + + overlap = common.pathOverlap(t[0], tt[0], root) + + if overlap & 1 + if t[0].length == tt[0].length + tt[3 + (root - tt[1]) * 2] = Math.max(tt[3 + (root - tt[1]) * 2], t[3 + (root - t[1]) * 2]) + else + tt[4 + (root - tt[1]) * 2] = Math.max(tt[4 + (root - tt[1]) * 2], t[4 + (root - t[1]) * 2]) + + if overlap & 2 + t[3 + (root - t[1]) * 2] = Math.max(t[3 + (root - t[1]) * 2], tt[3 + (root - t[1]) * 2]) + + return + + recursive = (ref, path, timestamps, timestampThreshold, revisionThreshold) -> + declaration = ref[0]?[0]?[0]?.slice() or [] + base = ref[0]?[0]?[1]?.slice() or [] + current = [] + + # Find timestamp and revision in which this parameter was most recently + # created in order to ignore older properties + for t in timestamps + for j in [Math.min(revision, t[2].length - 1)..1] by -1 when t[2][j]? + if not (t[2][j] <= ref[0]?[0]?[j]?[0]) and t[2][j] >= timestampThreshold + timestampThreshold = Math.max(timestampThreshold, t[2][j]) + revisionThreshold = Math.max(revisionThreshold, j) + current[0] = timestampThreshold + break + + # Collect current properties + if ref[0]?[0]? + for i in [revisionThreshold...Math.min(revision, ref[0][0].length)] by 1 when (rev = ref[0][0][i])? + for j in [0...rev.length] by 2 when rev[j]? + if rev[j] >= timestampThreshold + current[j] = rev[j] + current[j + 1] = rev[j + 1] + else + current[j] ?= 0 + + # Return if parameter no longer exists + if not (declaration[1] or base[1] or current[1]) + l = timestamps.length + collectTimestamps(ref, path, path.length - 1, timestamps) + processOverlaps(timestamps, path.length - 1, l) + return + + tentativeTimestamp = -1 + if current[2]? and not current[3] + tentativeTimestamp = current[2] + + filteredTimestamps = timestamps.filter((t) -> + if t[0][path.length - 1]? + if t[0][path.length - 1]? and t[0][path.length - 1] == path[path.length - 1] + t[2 + (path.length - t[1]) * 2] = Math.max(t[2 + (path.length - t[1]) * 2], current[0]) + else + return false + + if t[0].length > path.length + t[3 + (path.length - t[1]) * 2] = t[1 + (path.length - t[1]) * 2] + t[4 + (path.length - t[1]) * 2] = tentativeTimestamp + return true + + if not declaration[0] or t[2][0] > declaration[0] + declaration[0] = t[2][0] + + return false + ) + + collectTimestamps(ref, path, path.length, filteredTimestamps) + + processOverlaps(filteredTimestamps, path.length, 0) + + if path.length < pattern.length + children = {} + for k, v of ref[1] + if not pattern[path.length]? or k == pattern[path.length] + r = recursive(v, path.concat(k), filteredTimestamps, timestampThreshold, revisionThreshold) + children[k] = r if r? + + descendantTimestamps = filteredTimestamps.map((t) -> + res = [t[0], t[1]] + res[2] = t[2][0] if t[2][0]? + res[3] = t[2][1] if t[2][1]? + + cur = Math.max(t[3 + (path.length - t[1]) * 2], t[4 + (path.length - t[1]) * 2]) + + if path.length > t[1] + t[2 + (path.length - t[1]) * 2] = Math.min(t[2 + (path.length - t[1]) * 2], cur) + + if cur >= 0 + res[4] = cur + + return res + ).filter((t) -> t.length > 1) + + # Match value type of current if no type is specified + if declaration[7]? and not declaration[7][1]? + declaration[7] = sanitizeParameterValue([declaration[7][0], current[7]?[1]]) + + return callback(path, declaration, base, current, descendantTimestamps, children) + + return recursive(deviceData, [], [], 0, 1) + + +getPrerequisiteDeclarations = (declarations) -> + dec = [declarations[0]] + for i in [1...declarations.length] by 2 + if declarations[i]? + dec[i] = declarations[i] + else if declarations[i + 1]? + dec[i] = 0 + return [dec] + + +getAll = (deviceData, pattern, revision) -> + allParameters = [] + traverse(deviceData, pattern, revision, (path, declaration, base, current, descendantTimestamps, children) -> + if path.length == pattern.length and current[1] + allParameters.push([path].concat(current)) + ) + return allParameters + + +exports.getPrerequisiteDeclarations = getPrerequisiteDeclarations +exports.init = init +exports.getAll = getAll +exports.declare = declare +exports.set = set +exports.traverse = traverse