Support for regular expressions for paths in parameter configuration.

This commit is contained in:
Zaid Abdulla committed 2013-12-02 23:31:03 +03:00
1 parent 7695b606c6
commit 8c4240f024
8 files changed
+312 -123

No files matched your search

+8 -22
View File
@@ -20,20 +20,6 @@ INSTANCE_REGEX = /\.[\d]+\.$/
holdUntil = Date.now() holdUntil = Date.now()
applyConfigurations = (currentRequest, taskList) ->
if taskList.length
util.log("#{currentRequest.deviceId}: Presets discrepancy found")
apiFunctions.insertTasks(taskList, (err, tasks) ->
throw new Error(err) if err?
task = tasks[0]
util.log("#{currentRequest.deviceId}: Started task #{task.name}(#{task._id})")
runTask(currentRequest, task, {})
)
else
res = tr069.response(null, {}, {})
writeResponse(currentRequest, res)
writeResponse = (currentRequest, res) -> writeResponse = (currentRequest, res) ->
if res.headers['Content-Length'] == 0 if res.headers['Content-Length'] == 0
# no more requests. terminate TCP connection # no more requests. terminate TCP connection
@@ -122,14 +108,14 @@ updateDevice = (currentRequest, actions, callback) ->
return return
task = {device : currentRequest.deviceId, name : 'init', timestamp : config.INIT_TIMESTAMP} task = {device : currentRequest.deviceId, name : 'init', timestamp : config.INIT_TIMESTAMP}
apiFunctions.insertTasks(task, (err, t) -> apiFunctions.insertTasks(task, {}, (err, t) ->
callback(err) if callback? callback(err) if callback?
) )
) )
else if updates['_lastBootstrap']? else if updates['_lastBootstrap']?
# reinitialize on bootstrap event (e.g. firmware upgrade) # reinitialize on bootstrap event (e.g. firmware upgrade)
task = {device : currentRequest.deviceId, name : 'init', timestamp : config.INIT_TIMESTAMP} task = {device : currentRequest.deviceId, name : 'init', timestamp : config.INIT_TIMESTAMP}
apiFunctions.insertTasks(task, (err) -> apiFunctions.insertTasks(task, {}, (err) ->
callback(err) if callback? callback(err) if callback?
) )
else else
@@ -209,14 +195,14 @@ assertPresets = (currentRequest) ->
res = tr069.response(null, {}) res = tr069.response(null, {})
writeResponse(currentRequest, res) writeResponse(currentRequest, res)
else else
db.getPresets((allPresets, allObjects) -> db.getPresetsObjectsAliases((allPresets, allObjects, allAliases) ->
if not presetsHash? if not presetsHash?
presetsHash = presets.calculatePresetsHash(allPresets, allObjects) presetsHash = presets.calculatePresetsHash(allPresets, allObjects)
db.redisClient.setex('presets_hash', config.PRESETS_CACHE_DURATION, presetsHash, (err, res) -> db.redisClient.setex('presets_hash', config.PRESETS_CACHE_DURATION, presetsHash, (err, res) ->
throw err if err throw err if err
) )
presets.getDevicePreset(currentRequest.deviceId, allPresets, allObjects, (devicePreset) -> presets.getDevicePreset(currentRequest.deviceId, allPresets, allObjects, allAliases, (devicePreset) ->
presets.processDevicePreset(currentRequest.deviceId, devicePreset, (taskList, addTags, deleteTags, expiry) -> presets.processDevicePreset(currentRequest.deviceId, devicePreset, (taskList, addTags, deleteTags, expiry) ->
db.redisClient.setex("#{currentRequest.deviceId}_presets_hash", Math.floor(Math.max(1, expiry - config.PRESETS_TIME_PADDING)), presetsHash, (err, res) -> db.redisClient.setex("#{currentRequest.deviceId}_presets_hash", Math.floor(Math.max(1, expiry - config.PRESETS_TIME_PADDING)), presetsHash, (err, res) ->
throw err if err throw err if err
@@ -240,7 +226,7 @@ assertPresets = (currentRequest) ->
if taskList.length if taskList.length
t.expiry = expiry for t in taskList t.expiry = expiry for t in taskList
apiFunctions.insertTasks(taskList, (err, taskList) -> apiFunctions.insertTasks(taskList, allAliases, (err, taskList) ->
throw err if err throw err if err
task = taskList[0] task = taskList[0]
util.log("#{currentRequest.deviceId}: Started task #{task.name}(#{task._id})") util.log("#{currentRequest.deviceId}: Started task #{task.name}(#{task._id})")
@@ -358,8 +344,8 @@ listener = (httpRequest, httpResponse) ->
# Only supporting firmware upgrade for now # Only supporting firmware upgrade for now
return requestDownloadResponse() return requestDownloadResponse()
db.getPresets((allPresets, allObjects) -> db.getPresetsObjectsAliases((allPresets, allObjects, allAliases) ->
presets.getDevicePreset(currentRequest.deviceId, allPresets, allObjects, (devicePreset) -> presets.getDevicePreset(currentRequest.deviceId, allPresets, allObjects, allAliases, (devicePreset) ->
presetSoftwareVersion = devicePreset.softwareVersion?.preset presetSoftwareVersion = devicePreset.softwareVersion?.preset
currentSoftwareVersion = devicePreset.softwareVersion?.current._value currentSoftwareVersion = devicePreset.softwareVersion?.current._value
if presetSoftwareVersion? and presetSoftwareVersion != currentSoftwareVersion if presetSoftwareVersion? and presetSoftwareVersion != currentSoftwareVersion
@@ -381,7 +367,7 @@ listener = (httpRequest, httpResponse) ->
name : 'download', name : 'download',
file : file['_id'] file : file['_id']
} }
apiFunctions.insertTasks(task, (err, tasks) -> apiFunctions.insertTasks(task, allAliases, (err, tasks) ->
throw err if err throw err if err
return requestDownloadResponse() return requestDownloadResponse()
) )
+9 -9
View File
@@ -73,9 +73,9 @@ watchTask = (taskId, timeout, callback) ->
, 500) , 500)
expandParam = (param) -> expandParam = (param, aliases) ->
params = [param] params = [param]
for a,aa of config.ALIASES for a,aa of aliases
if a == param or common.startsWith(a, "#{param}.") if a == param or common.startsWith(a, "#{param}.")
for p in aa for p in aa
params.push(p) if p[p.lastIndexOf('.') + 1] != '_' params.push(p) if p[p.lastIndexOf('.') + 1] != '_'
@@ -83,7 +83,7 @@ expandParam = (param) ->
return params return params
sanitizeTask = (task, callback) -> sanitizeTask = (task, aliases, callback) ->
task.timestamp = new Date(task.timestamp ? Date.now()) task.timestamp = new Date(task.timestamp ? Date.now())
if task.expiry? if task.expiry?
if common.typeOf(task.expiry) is common.DATE_TYPE or isNaN(task.expiry) if common.typeOf(task.expiry) is common.DATE_TYPE or isNaN(task.expiry)
@@ -95,7 +95,7 @@ sanitizeTask = (task, callback) ->
when 'getParameterValues' when 'getParameterValues'
projection = {} projection = {}
for p in task.parameterNames for p in task.parameterNames
for pp in expandParam(p) for pp in expandParam(p, aliases)
projection[pp] = 1 projection[pp] = 1
db.devicesCollection.findOne({_id : task.device}, projection, (err, device) -> db.devicesCollection.findOne({_id : task.device}, projection, (err, device) ->
parameterNames = [] parameterNames = []
@@ -109,7 +109,7 @@ sanitizeTask = (task, callback) ->
projection = {} projection = {}
values = {} values = {}
for p in task.parameterValues for p in task.parameterValues
for pp in expandParam(p[0]) for pp in expandParam(p[0], aliases)
projection[pp] = 1 projection[pp] = 1
values[pp] = p[1] values[pp] = p[1]
db.devicesCollection.findOne({_id : task.device}, projection, (err, device) -> db.devicesCollection.findOne({_id : task.device}, projection, (err, device) ->
@@ -126,8 +126,8 @@ sanitizeTask = (task, callback) ->
callback(task) callback(task)
addAliases = (device) -> addAliases = (device, aliases) ->
for k,v of config.ALIASES for k,v of aliases
for p in v for p in v
pp = p.split('.') pp = p.split('.')
obj = device obj = device
@@ -140,14 +140,14 @@ addAliases = (device) ->
device[k] = obj if obj? device[k] = obj if obj?
insertTasks = (tasks, callback) -> insertTasks = (tasks, aliases, callback) ->
if common.typeOf(tasks) isnt common.ARRAY_TYPE if common.typeOf(tasks) isnt common.ARRAY_TYPE
tasks = [tasks] tasks = [tasks]
counter = tasks.length counter = tasks.length
for task in tasks for task in tasks
sanitizeTask(task, (t) -> sanitizeTask(task, aliases, (t) ->
if t.uniqueKey? if t.uniqueKey?
db.tasksCollection.remove({device : t.device, uniqueKey : t.uniqueKey}, (err, removed) -> db.tasksCollection.remove({device : t.device, uniqueKey : t.uniqueKey}, (err, removed) ->
) )
+80 -70
View File
@@ -164,33 +164,35 @@ else
if body if body
task = JSON.parse(body) task = JSON.parse(body)
task.device = deviceId task.device = deviceId
apiFunctions.insertTasks(task, (err) -> db.getAliases((aliases) ->
db.redisClient.del("#{deviceId}_presets_hash", (err) -> apiFunctions.insertTasks(task, aliases, (err) ->
throw err if err db.redisClient.del("#{deviceId}_presets_hash", (err) ->
) throw err if err
if err
response.writeHead(500)
response.end(err)
return
if urlParts.query.connection_request?
apiFunctions.connectionRequest(deviceId, (err) ->
if err
response.writeHead(202)
response.end()
else
apiFunctions.watchTask(task._id, config.DEVICE_ONLINE_THRESHOLD, (err) ->
if err
response.writeHead(202)
response.end()
return
response.writeHead(200)
response.end()
)
) )
else if err
response.writeHead(202) response.writeHead(500)
response.end() response.end(err)
return
if urlParts.query.connection_request?
apiFunctions.connectionRequest(deviceId, (err) ->
if err
response.writeHead(202)
response.end()
else
apiFunctions.watchTask(task._id, config.DEVICE_ONLINE_THRESHOLD, (err) ->
if err
response.writeHead(202)
response.end()
return
response.writeHead(200)
response.end()
)
)
else
response.writeHead(202)
response.end()
)
) )
else if urlParts.query.connection_request? else if urlParts.query.connection_request?
# no task, send connection request only # no task, send connection request only
@@ -211,8 +213,8 @@ else
else if DEVICE_PRESET_REGEX.test(urlParts.pathname) else if DEVICE_PRESET_REGEX.test(urlParts.pathname)
deviceId = querystring.unescape(DEVICE_PRESET_REGEX.exec(urlParts.pathname)[1]) deviceId = querystring.unescape(DEVICE_PRESET_REGEX.exec(urlParts.pathname)[1])
if request.method is 'GET' if request.method is 'GET'
db.getPresets((allPresets, allObjects) -> db.getPresetsObjectsAliases((allPresets, allObjects, allAliases) ->
presets.getDevicePreset(deviceId, allPresets, allObjects, (devicePreset) -> presets.getDevicePreset(deviceId, allPresets, allObjects, allAliases, (devicePreset) ->
response.writeHead(200, {'Content-Type' : 'application/json'}) response.writeHead(200, {'Content-Type' : 'application/json'})
response.end(JSON.stringify(devicePreset)) response.end(JSON.stringify(devicePreset))
) )
@@ -301,51 +303,59 @@ else
response.end('404 Not Found') response.end('404 Not Found')
return return
if urlParts.query.query? func = (aliases) ->
q = JSON.parse(querystring.unescape(urlParts.query.query)) if urlParts.query.query?
else q = JSON.parse(querystring.unescape(urlParts.query.query))
q = {} else
q = query.expand(q) if collectionName is 'devices' q = {}
q = query.expand(q, aliases) if collectionName is 'devices'
if urlParts.query.projection? if urlParts.query.projection?
projection = {} projection = {}
for p in urlParts.query.projection.split(',') for p in urlParts.query.projection.split(',')
p = p.trim() p = p.trim()
projection[p] = 1 projection[p] = 1
if collectionName is 'devices' if collectionName is 'devices'
for k,v of config.ALIASES for k,v of aliases
if k == p or common.startsWith(k, "#{p}.") if k == p or common.startsWith(k, "#{p}.")
projection[a] = 1 for a in v projection[a] = 1 for a in v
cur = collection.find(q, projection, {batchSize : 50}) cur = collection.find(q, projection, {batchSize : 50})
if urlParts.query.sort? if urlParts.query.sort?
s = JSON.parse(querystring.unescape(urlParts.query.sort)) s = JSON.parse(querystring.unescape(urlParts.query.sort))
sort = {} sort = {}
for k,v of s for k,v of s
if config.ALIASES[k]? if aliases[k]?
sort[a] = v for a in config.ALIASES[k] sort[a] = v for a in aliases[k]
else else
sort[k] = v sort[k] = v
cur.sort(sort) cur.sort(sort)
cur.skip(parseInt(urlParts.query.skip)) if urlParts.query.skip? cur.skip(parseInt(urlParts.query.skip)) if urlParts.query.skip?
cur.limit(parseInt(urlParts.query.limit)) if urlParts.query.limit? cur.limit(parseInt(urlParts.query.limit)) if urlParts.query.limit?
cur.count((err, total) -> cur.count((err, total) ->
response.writeHead(200, {'Content-Type' : 'application/json', 'total' : total}) response.writeHead(200, {'Content-Type' : 'application/json', 'total' : total})
if request.method is 'HEAD' if request.method is 'HEAD'
response.end() response.end()
return return
response.write("[\n") response.write("[\n")
i = 0 i = 0
cur.each((err, item) -> cur.each((err, item) ->
if item is null if item is null
response.end("\n]") response.end("\n]")
else else
response.write(",\n") if i++ response.write(",\n") if i++
apiFunctions.addAliases(item) if collectionName is 'devices' apiFunctions.addAliases(item, aliases) if collectionName is 'devices'
response.write(JSON.stringify(item)) response.write(JSON.stringify(item))
)
) )
)
if collectionName is 'devices'
db.getAliases((aliases) ->
func(aliases)
)
else
func({})
else else
response.writeHead 404 response.writeHead 404
response.end('404 Not Found') response.end('404 Not Found')
+25 -8
View File
@@ -1,6 +1,7 @@
config = require './config' config = require './config'
mongodb = require 'mongodb' mongodb = require 'mongodb'
redisClient = require('redis').createClient(config.REDIS_SOCKET) redisClient = require('redis').createClient(config.REDIS_SOCKET)
parameters = require './parameters'
tasksCollection = null tasksCollection = null
devicesCollection = null devicesCollection = null
@@ -98,13 +99,21 @@ getCached = (name, valueCallback, valueExpiry, callback) ->
) )
getPresets = (callback) -> getAliases = (callback) ->
redisClient.mget('presets', 'objects', (err, res) -> getCached('aliases', parameters.compileAliases, config.PRESETS_CACHE_DURATION, (err, res) ->
throw err if err
callback(res)
)
getPresetsObjectsAliases = (callback) ->
redisClient.mget('presets', 'objects', 'aliases', (err, res) ->
presets = JSON.parse(res[0]) presets = JSON.parse(res[0])
objects = JSON.parse(res[1]) objects = JSON.parse(res[1])
if presets and objects aliases = JSON.parse(res[2])
callback(presets, objects)
return if presets and objects and aliases
return callback(presets, objects, aliases)
if not presets if not presets
getCached('presets', (callback) -> getCached('presets', (callback) ->
@@ -114,7 +123,7 @@ getPresets = (callback) ->
, config.PRESETS_CACHE_DURATION, (err, res) -> , config.PRESETS_CACHE_DURATION, (err, res) ->
throw err if err throw err if err
presets = res presets = res
callback(presets, objects) if objects callback(presets, objects, aliases) if objects and aliases
) )
if not objects if not objects
@@ -128,11 +137,19 @@ getPresets = (callback) ->
, config.PRESETS_CACHE_DURATION, (err, res) -> , config.PRESETS_CACHE_DURATION, (err, res) ->
throw err if err throw err if err
objects = res objects = res
callback(presets, objects) if presets callback(presets, objects, aliases) if presets and aliases
)
if not aliases
getCached('aliases', parameters.compileAliases, config.PRESETS_CACHE_DURATION, (err, res) ->
throw err if err
aliases = res
callback(presets, objects, aliases) if presets and objects
) )
) )
exports.redisClient = redisClient exports.redisClient = redisClient
exports.getTask = getTask exports.getTask = getTask
exports.getPresets = getPresets exports.getPresetsObjectsAliases = getPresetsObjectsAliases
exports.getAliases = getAliases
+3 -5
View File
@@ -1,5 +1,5 @@
config = require './config'
common = require './common' common = require './common'
parameters = require './parameters'
stringToRegexp = (input) -> stringToRegexp = (input) ->
@@ -105,7 +105,5 @@ normalizers.mac = (input, normType) ->
exports.normalize = (path, value, normType) -> exports.normalize = (path, value, normType) ->
if path of config.PARAMETERS and config.PARAMETERS[path].type? type = parameters.getType(path) ? 'default'
return normalizers[config.PARAMETERS[path].type](value, normType) normalizers[type](value, normType)
else
return normalizers['default'](value, normType)
+179
View File
@@ -0,0 +1,179 @@
config = require './config'
db = require './db'
PATH_REGEX = /\w+|(\/.+?\/\w*)/g
# Javascript represents numbers as 64-bit floating point numbers
# Integers up to 2^53 can be encoded precisely
MAX_PARAMETERS = 52
# get the index of the least significant bit
getLsb = (num) ->
i = -1
while num > 0
break if num & 1
num >>= 1
++ i
return i
# compile types into data types optimized for quick lookup
types = []
stringSegments = {}
regexSegments = {}
paramRegex = {}
for k, v of config.PARAMETERS
continue if not v.type?
typeIndex = types.push(v.type) - 1
throw new Error("There cannot be more than #{MAX_PARAMETERS} configured parameters") if typeIndex >= MAX_PARAMETERS
segmentIndex = 0
parts = k.match(PATH_REGEX)
for i in [0 ... parts.length]
part = parts[i]
positionHash = i + parts.length * MAX_PARAMETERS
if part[0] == '/' # this is a regex part
j = part.lastIndexOf('/')
regExp = new RegExp(part[1...j], part[j+1..])
regexSegments[positionHash] ?= 0
regexSegments[positionHash] |= Math.pow(2, typeIndex)
paramRegex[typeIndex] ?= {}
paramRegex[typeIndex][i] = regExp
else
stringSegments[positionHash] ?= {}
stringSegments[positionHash][part] ?= 0
stringSegments[positionHash][part] |= Math.pow(2, typeIndex)
getType = (param) ->
parts = param.split('.')
positionHash = parts.length * MAX_PARAMETERS
stringIndices = 0 | stringSegments[positionHash]?[parts[0]]
allIndices = stringIndices | regexSegments[positionHash]
for i in [1...parts.length]
positionHash = i + parts.length * MAX_PARAMETERS
strIdx = stringSegments[positionHash]?[parts[i]]
rgxIdx = regexSegments[positionHash]
stringIndices &= strIdx
allIndices &= (strIdx | rgxIdx)
if stringIndices > 0
return types[getLsb(stringIndices)]
typeIndex = 0
while allIndices > 0
if allIndices & 1
match = true
for i, r of paramRegex[typeIndex]
if not r.test(parts[i])
match = false
break
if match
return types[typeIndex]
++ typeIndex
allIndices >>= 1
return null
splitIds = (batches, callback) ->
ids = [{}]
return callback(ids) if batches == 1
db.devicesCollection.count((err, count) ->
return callback(ids) if count <= 1000
batchSize = Math.floor(count / batches)
for i in [1 ... batches]
ids[i] = {}
do (i) ->
cursor = db.devicesCollection.find({}, {_id:1}).sort({'_id':1}).skip(i * batchSize).limit(1)
cursor.nextObject((err, obj) ->
ids[i]['$gte'] = obj._id
ids[i-1]['$lt'] = obj._id
if i == batches - 1
callback(ids)
)
)
compileAliases = (callback) ->
map = () ->
for key, parts of keys
ref = this
path = ''
for part in parts
if part.test?
r = ref
ref = null
for p of r
if part.test(p)
path += ".#{p}"
ref = r[p]
break
else
path += ".#{part}"
ref = ref[part]
break if not ref
emit(path[1..], key) if ref
return
reduce = (k, v) ->
return v[0]
aliases = {}
keys = {}
for k, v of config.PARAMETERS
alias = v.alias
continue if not alias?
isStatic = true
ar = []
for p in k.match(/\w+|(\/.+?\/\w*)/g)
if p[0] == '/' # this is a regex
isStatic = false
i = p.lastIndexOf('/')
r = new RegExp(p[1...i], p[i+1..])
ar.push(r)
else
ar.push(p)
if isStatic
if not aliases[alias]?
aliases[alias] = []
aliases[alias].push(k)
else
keys[k] = ar
counter = 0
splitIds(4, (batches) ->
for batch in batches
options = {
out : {inline : 1}
jsMode : true
sort : {$natural : 1}
scope : {keys : keys, flags : {}}
query : {_id : batch} if batches.length > 1
}
db.devicesCollection.mapReduce(map, reduce, options, (err, out) ->
for o in out
alias = config.PARAMETERS[o.value].alias
if not aliases[alias]?
aliases[alias] = []
id = String(o._id)
if id not in aliases[alias]
aliases[alias].push(id)
if ++counter == batches.length
callback(err, aliases, config.ALIASES_CACHE)
)
)
exports.getType = getType
exports.compileAliases = compileAliases
+2 -2
View File
@@ -22,12 +22,12 @@ matchObject = (object, param) ->
return true return true
exports.getDevicePreset = (deviceId, presets, objects, callback) -> exports.getDevicePreset = (deviceId, presets, objects, aliases, callback) ->
deviceCustomCommands = customCommands.getDeviceCustomCommands(deviceId) deviceCustomCommands = customCommands.getDeviceCustomCommands(deviceId)
# only fetch relevant params # only fetch relevant params
projection = {_id : 1} projection = {_id : 1}
for p in presets for p in presets
p.precondition = query.expand(p.precondition) p.precondition = query.expand(p.precondition, aliases)
mongoQuery.projection(p.precondition, projection) mongoQuery.projection(p.precondition, projection)
for c in p.configurations for c in p.configurations
+6 -7
View File
@@ -1,4 +1,3 @@
config = require './config'
common = require './common' common = require './common'
normalize = require './normalize' normalize = require './normalize'
@@ -40,10 +39,10 @@ expandValue = (param, value) ->
return objs return objs
permute = (param, val) -> permute = (param, val, aliases) ->
keys = [] keys = []
if config.ALIASES[param]? if aliases[param]?
for p in config.ALIASES[param] for p in aliases[param]
keys.push(p) keys.push(p)
else else
keys.push(param) keys.push(param)
@@ -62,16 +61,16 @@ permute = (param, val) ->
return conditions return conditions
expand = (query) -> expand = (query, aliases) ->
new_query = {} new_query = {}
for k,v of query for k,v of query
if k[0] == '$' # operator if k[0] == '$' # operator
expressions = [] expressions = []
for e in v for e in v
expressions.push(expand(e)) expressions.push(expand(e, aliases))
new_query[k] = expressions new_query[k] = expressions
else else
conditions = permute(k, v) conditions = permute(k, v, aliases)
if conditions.length > 1 if conditions.length > 1
if new_query['$and']? if new_query['$and']?
new_query['$and'].push({'$or' : conditions}) new_query['$and'].push({'$or' : conditions})