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