Optimize XML parsing
Using XPath expressions appears to be expensive so that has been replaced with direct child node access. This seems to have improved performance by 5%. A side effect of this change is that XML_IGNORE_NAMESPACE option is no longer needed as namespaces are ignored by default.
This commit is contained in:
1 parent
037aa9786d
commit
ec215ffb82
2 files changed
+293
-175
No files matched your search
@@ -79,7 +79,6 @@ const options = {
|
||||
XML_FORMAT: { type: "bool", default: false },
|
||||
XML_NO_DECL: { type: "bool", default: false },
|
||||
XML_NO_EMPTY: { type: "bool", default: false },
|
||||
XML_IGNORE_NAMESPACE: { type: "bool", default: false },
|
||||
|
||||
// Should probably never be changed
|
||||
DEVICE_ONLINE_THRESHOLD: { type: "int", default: 4000 }
|
||||
@@ -106,9 +105,6 @@ function setConfig(name, value, commandLineArgument) {
|
||||
)
|
||||
setConfig("GPV_BATCH_SIZE", value);
|
||||
|
||||
if (name === "IGNORE_XML_NAMESPACES" || name === "ignore-xml-namespaces")
|
||||
setConfig("XML_IGNORE_NAMESPACE", value);
|
||||
|
||||
if (name === "XML_PARSE_IGNORE_ENC" || name === "xml-parse-ignore-enc")
|
||||
setConfig("XML_IGNORE_ENC", value);
|
||||
|
||||
|
||||
+293
-171
@@ -74,18 +74,6 @@ const LIBXMLJS_SAVE_OPTIONS = {
|
||||
selfCloseEmpty: !config.get("XML_NO_EMPTY")
|
||||
};
|
||||
|
||||
const XML_IGNORE_NAMESPACE = config.get("XML_IGNORE_NAMESPACE");
|
||||
|
||||
if (XML_IGNORE_NAMESPACE) {
|
||||
libxmljs.Element.prototype.__find = libxmljs.Element.prototype.find;
|
||||
libxmljs.Element.prototype.find = function(xpath, namespaces) {
|
||||
const p = xpath
|
||||
.replace(/([^/:]+:)?([^/]+)(\/|$)/g, "$2$3")
|
||||
.replace(/([a-zA-Z0-9_-]+)([^/]*)(\/|$)/g, '*[local-name()="$1"]$2$3');
|
||||
return libxmljs.Element.prototype.__find.call(this, p, namespaces);
|
||||
};
|
||||
}
|
||||
|
||||
let warnings;
|
||||
|
||||
function parseBool(v) {
|
||||
@@ -97,65 +85,100 @@ function parseBool(v) {
|
||||
}
|
||||
|
||||
function event(xml) {
|
||||
return xml.find("EventStruct/EventCode").map(e => e.text().trim());
|
||||
return xml
|
||||
.childNodes()
|
||||
.filter(n => n.name() === "EventStruct")
|
||||
.map(c =>
|
||||
c
|
||||
.childNodes()
|
||||
.find(n => n.name() === "EventCode")
|
||||
.text()
|
||||
.trim()
|
||||
);
|
||||
}
|
||||
|
||||
function parameterInfoList(xml) {
|
||||
return xml.find("ParameterInfoStruct").map(e => {
|
||||
const param = e.get("Name").text();
|
||||
const value = e.get("Writable").text();
|
||||
let parsed = parseBool(value);
|
||||
return xml
|
||||
.childNodes()
|
||||
.filter(e => e.name() === "ParameterInfoStruct")
|
||||
.map(e => {
|
||||
let param, value;
|
||||
for (const c of e.childNodes()) {
|
||||
switch (c.name()) {
|
||||
case "Name":
|
||||
param = c.text();
|
||||
break;
|
||||
case "Writable":
|
||||
value = c.text();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (parsed === null) {
|
||||
warnings.push({
|
||||
message: "Invalid writable attribute",
|
||||
parameter: param
|
||||
});
|
||||
parsed = false;
|
||||
}
|
||||
let parsed = parseBool(value);
|
||||
|
||||
return [param, parsed];
|
||||
});
|
||||
if (parsed === null) {
|
||||
warnings.push({
|
||||
message: "Invalid writable attribute",
|
||||
parameter: param
|
||||
});
|
||||
parsed = false;
|
||||
}
|
||||
|
||||
return [param, parsed];
|
||||
});
|
||||
}
|
||||
|
||||
function parameterValueList(xml) {
|
||||
return xml.find("ParameterValueStruct").map(e => {
|
||||
const valueElement = e.get("Value");
|
||||
const valueType = valueElement.attr("type").value();
|
||||
const param = e.get("Name").text();
|
||||
const value = valueElement.text();
|
||||
let parsed = value;
|
||||
if (valueType === "xsd:boolean") {
|
||||
parsed = parseBool(value);
|
||||
if (parsed === null) {
|
||||
warnings.push({
|
||||
message: "Invalid value attribute",
|
||||
parameter: param
|
||||
});
|
||||
parsed = value;
|
||||
return xml
|
||||
.childNodes()
|
||||
.filter(e => e.name() === "ParameterValueStruct")
|
||||
.map(e => {
|
||||
let valueElement, param;
|
||||
for (const c of e.childNodes()) {
|
||||
switch (c.name()) {
|
||||
case "Name":
|
||||
param = c.text();
|
||||
break;
|
||||
case "Value":
|
||||
valueElement = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (valueType === "xsd:int" || valueType === "xsd:unsignedInt") {
|
||||
parsed = parseInt(value);
|
||||
if (isNaN(parsed)) {
|
||||
warnings.push({
|
||||
message: "Invalid value attribute",
|
||||
parameter: param
|
||||
});
|
||||
parsed = value;
|
||||
}
|
||||
} else if (valueType === "xsd:dateTime") {
|
||||
parsed = Date.parse(value);
|
||||
if (isNaN(parsed)) {
|
||||
warnings.push({
|
||||
message: "Invalid value attribute",
|
||||
parameter: param
|
||||
});
|
||||
parsed = value;
|
||||
}
|
||||
}
|
||||
|
||||
return [param, parsed, valueType];
|
||||
});
|
||||
const valueType = valueElement.attr("type").value();
|
||||
const value = valueElement.text();
|
||||
let parsed = value;
|
||||
if (valueType === "xsd:boolean") {
|
||||
parsed = parseBool(value);
|
||||
if (parsed === null) {
|
||||
warnings.push({
|
||||
message: "Invalid value attribute",
|
||||
parameter: param
|
||||
});
|
||||
parsed = value;
|
||||
}
|
||||
} else if (valueType === "xsd:int" || valueType === "xsd:unsignedInt") {
|
||||
parsed = parseInt(value);
|
||||
if (isNaN(parsed)) {
|
||||
warnings.push({
|
||||
message: "Invalid value attribute",
|
||||
parameter: param
|
||||
});
|
||||
parsed = value;
|
||||
}
|
||||
} else if (valueType === "xsd:dateTime") {
|
||||
parsed = Date.parse(value);
|
||||
if (isNaN(parsed)) {
|
||||
warnings.push({
|
||||
message: "Invalid value attribute",
|
||||
parameter: param
|
||||
});
|
||||
parsed = value;
|
||||
}
|
||||
}
|
||||
|
||||
return [param, parsed, valueType];
|
||||
});
|
||||
}
|
||||
|
||||
function GetParameterNames(xml, methodRequest) {
|
||||
@@ -167,7 +190,9 @@ function GetParameterNames(xml, methodRequest) {
|
||||
function GetParameterNamesResponse(xml) {
|
||||
return {
|
||||
name: "GetParameterNamesResponse",
|
||||
parameterList: parameterInfoList(xml.get("ParameterList"))
|
||||
parameterList: parameterInfoList(
|
||||
xml.childNodes().find(n => n.name() === "ParameterList")
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -183,7 +208,9 @@ function GetParameterValues(xml, methodRequest) {
|
||||
function GetParameterValuesResponse(xml) {
|
||||
return {
|
||||
name: "GetParameterValuesResponse",
|
||||
parameterList: parameterValueList(xml.get("ParameterList"))
|
||||
parameterList: parameterValueList(
|
||||
xml.childNodes().find(n => n.name() === "ParameterList")
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -220,7 +247,12 @@ function SetParameterValues(xml, methodRequest) {
|
||||
function SetParameterValuesResponse(xml) {
|
||||
return {
|
||||
name: "SetParameterValuesResponse",
|
||||
status: parseInt(xml.get("Status").text())
|
||||
status: parseInt(
|
||||
xml
|
||||
.childNodes()
|
||||
.find(n => n.name() === "Status")
|
||||
.text()
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -231,10 +263,22 @@ function AddObject(xml, methodRequest) {
|
||||
}
|
||||
|
||||
function AddObjectResponse(xml) {
|
||||
let instanceNumber, status;
|
||||
for (const c of xml.childNodes()) {
|
||||
switch (c.name()) {
|
||||
case "InstanceNumber":
|
||||
instanceNumber = parseInt(c.text());
|
||||
break;
|
||||
case "Status":
|
||||
status = parseInt(c.text());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: "AddObjectResponse",
|
||||
instanceNumber: parseInt(xml.get("InstanceNumber").text()),
|
||||
status: parseInt(xml.get("Status").text())
|
||||
instanceNumber: instanceNumber,
|
||||
status: status
|
||||
};
|
||||
}
|
||||
|
||||
@@ -247,7 +291,12 @@ function DeleteObject(xml, methodRequest) {
|
||||
function DeleteObjectResponse(xml) {
|
||||
return {
|
||||
name: "DeleteObjectResponse",
|
||||
status: parseInt(xml.get("Status").text())
|
||||
status: parseInt(
|
||||
xml
|
||||
.childNodes()
|
||||
.find(n => n.name() === "Status")
|
||||
.text()
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -287,31 +336,64 @@ function Download(xml, methodRequest) {
|
||||
}
|
||||
|
||||
function DownloadResponse(xml) {
|
||||
const res = {
|
||||
name: "DownloadResponse",
|
||||
status: parseInt(xml.get("Status").text())
|
||||
};
|
||||
|
||||
if (res.status === 0) {
|
||||
res.startTime = Date.parse(xml.get("StartTime").text());
|
||||
res.completeTime = Date.parse(xml.get("CompleteTime").text());
|
||||
let status, startTime, completeTime;
|
||||
for (const c of xml.childNodes()) {
|
||||
switch (c.name()) {
|
||||
case "Status":
|
||||
status = parseInt(c.text());
|
||||
break;
|
||||
case "StartTime":
|
||||
startTime = Date.parse(c.text());
|
||||
break;
|
||||
case "CompleteTime":
|
||||
completeTime = Date.parse(c.text());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
return {
|
||||
name: "DownloadResponse",
|
||||
status: status,
|
||||
startTime: startTime,
|
||||
completeTime: completeTime
|
||||
};
|
||||
}
|
||||
|
||||
function Inform(xml) {
|
||||
let retryCount, evnt, parameterList;
|
||||
const deviceId = {
|
||||
Manufacturer: null,
|
||||
OUI: null,
|
||||
ProductClass: null,
|
||||
SerialNumber: null
|
||||
};
|
||||
|
||||
for (const c of xml.childNodes()) {
|
||||
switch (c.name()) {
|
||||
case "ParameterList":
|
||||
parameterList = parameterValueList(c);
|
||||
break;
|
||||
case "DeviceId":
|
||||
for (const cc of c.childNodes()) {
|
||||
const n = cc.name();
|
||||
if (n in deviceId) deviceId[n] = cc.text();
|
||||
}
|
||||
break;
|
||||
case "Event":
|
||||
evnt = event(c);
|
||||
break;
|
||||
case "RetryCount":
|
||||
retryCount = parseInt(c.text());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: "Inform",
|
||||
parameterList: parameterValueList(xml.get("ParameterList")),
|
||||
deviceId: {
|
||||
Manufacturer: xml.get("DeviceId/Manufacturer").text(),
|
||||
OUI: xml.get("DeviceId/OUI").text(),
|
||||
ProductClass: xml.get("DeviceId/ProductClass").text(),
|
||||
SerialNumber: xml.get("DeviceId/SerialNumber").text()
|
||||
},
|
||||
event: event(xml.get("Event")),
|
||||
retryCount: parseInt(xml.get("RetryCount").text())
|
||||
parameterList: parameterList,
|
||||
deviceId: deviceId,
|
||||
event: evnt,
|
||||
retryCount: retryCount
|
||||
};
|
||||
}
|
||||
|
||||
@@ -336,12 +418,30 @@ function GetRPCMethodsResponse(xml, methodResponse) {
|
||||
}
|
||||
|
||||
function TransferComplete(xml) {
|
||||
let commandKey, _faultStruct, startTime, completeTime;
|
||||
for (const c of xml.childNodes()) {
|
||||
switch (c.name()) {
|
||||
case "CommandKey":
|
||||
commandKey = c.text();
|
||||
break;
|
||||
case "FaultStruct":
|
||||
_faultStruct = faultStruct(c);
|
||||
break;
|
||||
case "StartTime":
|
||||
startTime = Date.parse(c.text());
|
||||
break;
|
||||
case "CompleteTime":
|
||||
completeTime = Date.parse(c.text());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: "TransferComplete",
|
||||
commandKey: xml.get("CommandKey").text(),
|
||||
faultStruct: faultStruct(xml.get("FaultStruct")),
|
||||
startTime: Date.parse(xml.get("StartTime").text()),
|
||||
completeTime: Date.parse(xml.get("CompleteTime").text())
|
||||
commandKey: commandKey,
|
||||
faultStruct: _faultStruct,
|
||||
startTime: startTime,
|
||||
completeTime: completeTime
|
||||
};
|
||||
}
|
||||
|
||||
@@ -352,7 +452,10 @@ function TransferCompleteResponse(xml) {
|
||||
function RequestDownload(xml) {
|
||||
return {
|
||||
name: "RequestDownload",
|
||||
fileType: xml.get("FileType").text()
|
||||
fileType: xml
|
||||
.childNodes()
|
||||
.find(n => n.name() === "FileType")
|
||||
.text()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -361,103 +464,125 @@ function RequestDownloadResponse(xml) {
|
||||
}
|
||||
|
||||
function faultStruct(xml) {
|
||||
const f = {
|
||||
faultCode: xml.get("FaultCode").text(),
|
||||
faultString: xml.get("FaultString").text()
|
||||
};
|
||||
|
||||
for (const e of xml.find("SetParameterValuesFault")) {
|
||||
if (!f.setParameterValuesFault) f.setParameterValuesFault = [];
|
||||
|
||||
f.setParameterValuesFault.push({
|
||||
parameterName: e.get("ParameterName").text(),
|
||||
faultCode: e.get("FaultCode").text(),
|
||||
faultString: e.get("FaultString").text()
|
||||
});
|
||||
let faultCode, faultString, setParameterValuesFault, pn, fc, fs;
|
||||
for (const c of xml.childNodes()) {
|
||||
switch (c.name()) {
|
||||
case "FaultCode":
|
||||
faultCode = c.text();
|
||||
break;
|
||||
case "FaultString":
|
||||
faultString = c.text();
|
||||
break;
|
||||
case "SetParameterValuesFault":
|
||||
setParameterValuesFault = setParameterValuesFault || [];
|
||||
for (const cc of c.childNodes()) {
|
||||
switch (cc.name()) {
|
||||
case "ParameterName":
|
||||
pn = cc.text();
|
||||
break;
|
||||
case "FaultCode":
|
||||
fc = cc.text();
|
||||
break;
|
||||
case "FaultString":
|
||||
fs = cc.text();
|
||||
break;
|
||||
}
|
||||
}
|
||||
setParameterValuesFault.push({
|
||||
parameterName: pn,
|
||||
faultCode: fc,
|
||||
faultString: fs
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return f;
|
||||
return { faultCode, faultString, setParameterValuesFault };
|
||||
}
|
||||
|
||||
function fault(xml, cwmpVersion) {
|
||||
return {
|
||||
faultCode: xml.get("faultcode").text(),
|
||||
faultString: xml.get("faultstring").text(),
|
||||
detail: faultStruct(xml.get("detail/cwmp:Fault", NAMESPACES[cwmpVersion]))
|
||||
};
|
||||
function fault(xml) {
|
||||
let faultCode, faultString, detail;
|
||||
for (const c of xml.childNodes) {
|
||||
switch (c.name()) {
|
||||
case "faultcode":
|
||||
faultCode = c.text();
|
||||
break;
|
||||
case "faultstring":
|
||||
faultString = c.text();
|
||||
break;
|
||||
case "detail":
|
||||
detail = faultStruct(c.childNodes().find(n => n.name() === "Fault"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { faultCode, faultString, detail };
|
||||
}
|
||||
|
||||
function request(data, cwmpVersion, warn) {
|
||||
warnings = warn;
|
||||
|
||||
const rpc = {
|
||||
cwmpVersion: cwmpVersion
|
||||
id: null,
|
||||
cwmpVersion: cwmpVersion,
|
||||
sessionTimeout: null,
|
||||
cpeRequest: null,
|
||||
cpeFault: null
|
||||
};
|
||||
|
||||
if (!data.length) return rpc;
|
||||
|
||||
const xml = libxmljs.parseXml(data, LIBXMLJS_PARSE_OPTIONS);
|
||||
|
||||
let methodElement;
|
||||
let headerElement, bodyElement;
|
||||
|
||||
if (rpc.cwmpVersion) {
|
||||
methodElement = xml.get(
|
||||
"/soap-env:Envelope/soap-env:Body/cwmp:*",
|
||||
NAMESPACES[rpc.cwmpVersion]
|
||||
);
|
||||
} else {
|
||||
// cwmpVersion not passed, thus it's an inform request
|
||||
methodElement = xml.get(
|
||||
"/soap-env:Envelope/soap-env:Body/*",
|
||||
NAMESPACES["1.0"]
|
||||
);
|
||||
let timeoutElement;
|
||||
switch (methodElement.namespace().href()) {
|
||||
case "urn:dslforum-org:cwmp-1-0":
|
||||
rpc.cwmpVersion = "1.0";
|
||||
for (const c of xml.childNodes()) {
|
||||
switch (c.name()) {
|
||||
case "Header":
|
||||
headerElement = c;
|
||||
break;
|
||||
case "urn:dslforum-org:cwmp-1-1":
|
||||
rpc.cwmpVersion = "1.1";
|
||||
case "Body":
|
||||
bodyElement = c;
|
||||
break;
|
||||
case "urn:dslforum-org:cwmp-1-2":
|
||||
timeoutElement = xml.get(
|
||||
"/soap-env:Envelope/soap-env:Header/cwmp:sessionTimeout",
|
||||
NAMESPACES["1.2"]
|
||||
);
|
||||
if (timeoutElement) {
|
||||
rpc.sessionTimeout = parseInt(timeoutElement.text());
|
||||
rpc.cwmpVersion = "1.3";
|
||||
} else {
|
||||
rpc.cwmpVersion = "1.2";
|
||||
}
|
||||
break;
|
||||
case "urn:dslforum-org:cwmp-1-3":
|
||||
rpc.cwmpVersion = "1.4";
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unrecognized CWMP version");
|
||||
}
|
||||
|
||||
if (!rpc.sessionTimeout) {
|
||||
timeoutElement = xml.get(
|
||||
"/soap-env:Envelope/soap-env:Header/cwmp:sessionTimeout",
|
||||
NAMESPACES[rpc.cwmpVersion]
|
||||
);
|
||||
if (timeoutElement) rpc.sessionTimeout = parseInt(timeoutElement.text());
|
||||
}
|
||||
}
|
||||
|
||||
const idElement = xml.get(
|
||||
"/soap-env:Envelope/soap-env:Header/cwmp:ID",
|
||||
NAMESPACES[rpc.cwmpVersion]
|
||||
);
|
||||
if (idElement) rpc.id = idElement.text();
|
||||
for (const c of headerElement.childNodes()) {
|
||||
switch (c.name()) {
|
||||
case "ID":
|
||||
rpc.id = c.text();
|
||||
break;
|
||||
case "sessionTimeout":
|
||||
rpc.sessionTimeout = parseInt(c.text());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
methodElement &&
|
||||
!(XML_IGNORE_NAMESPACE && methodElement.name() === "Fault")
|
||||
) {
|
||||
switch (methodElement.name()) {
|
||||
for (const methodElement of bodyElement.childNodes()) {
|
||||
const methodName = methodElement.name();
|
||||
if (methodName === "text") continue;
|
||||
|
||||
if (!rpc.cwmpVersion && methodName !== "Fault") {
|
||||
switch (methodElement.namespace().href()) {
|
||||
case "urn:dslforum-org:cwmp-1-0":
|
||||
rpc.cwmpVersion = "1.0";
|
||||
break;
|
||||
case "urn:dslforum-org:cwmp-1-1":
|
||||
rpc.cwmpVersion = "1.1";
|
||||
break;
|
||||
case "urn:dslforum-org:cwmp-1-2":
|
||||
if (rpc.sessionTimeout) rpc.cwmpVersion = "1.3";
|
||||
else rpc.cwmpVersion = "1.2";
|
||||
|
||||
break;
|
||||
case "urn:dslforum-org:cwmp-1-3":
|
||||
rpc.cwmpVersion = "1.4";
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unrecognized CWMP version");
|
||||
}
|
||||
}
|
||||
|
||||
switch (methodName) {
|
||||
case "Inform":
|
||||
rpc.cpeRequest = Inform(methodElement);
|
||||
break;
|
||||
@@ -494,15 +619,12 @@ function request(data, cwmpVersion, warn) {
|
||||
case "DownloadResponse":
|
||||
rpc.cpeResponse = DownloadResponse(methodElement);
|
||||
break;
|
||||
case "Fault":
|
||||
rpc.cpeFault = fault(methodElement, rpc.cwmpVersion);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`8000 Method not supported ${methodElement.name()}`);
|
||||
throw new Error(`8000 Method not supported ${methodName}`);
|
||||
}
|
||||
} else {
|
||||
const faultElement = xml.get(
|
||||
"/soap-env:Envelope/soap-env:Body/soap-env:Fault",
|
||||
NAMESPACES[rpc.cwmpVersion]
|
||||
);
|
||||
rpc.cpeFault = fault(faultElement, rpc.cwmpVersion);
|
||||
}
|
||||
|
||||
return rpc;
|
||||
|
||||
Reference in new issue
Block a user