From 6e4effda06d4d0d421fe06a4ca99681e92dee3e9 Mon Sep 17 00:00:00 2001 From: Jerry Padgett Date: Wed, 3 Aug 2022 12:48:59 -0400 Subject: [PATCH] Manage CCDA data transmission to service (#5658) * Manage CCDA data transmission to service - CCM chunk send data to service - add data stacking buffer to service - honor EOT(file separator character) in service - repace deprecated js substr with substring * - add reading from stack by a delimiter method for future use. Helps make clear how fetchBuffer is supposed to work. --- ccdaservice/serveccda.js | 97 ++++++++++++++++------ .../Model/CcdaServiceDocumentRequestor.php | 46 +++++----- 2 files changed, 98 insertions(+), 45 deletions(-) diff --git a/ccdaservice/serveccda.js b/ccdaservice/serveccda.js index 5a6ea498e..95ca939ee 100644 --- a/ccdaservice/serveccda.js +++ b/ccdaservice/serveccda.js @@ -26,6 +26,48 @@ var webRoot = ""; var authorDateTime = ''; var documentLocation = ''; +class DataStack { + constructor(delimiter) { + this.delimiter = delimiter; + this.buffer = ""; + } + + endOfCcda() { + return this.buffer.length === 0 || this.buffer.indexOf(this.delimiter) === -1; + } + + pushToStack(data) { + this.buffer += data; + } + + fetchBuffer() { + const delimiterIndex = this.buffer.indexOf(this.delimiter); + if (delimiterIndex !== -1) { + const bufferMsg = this.buffer.slice(0, delimiterIndex); + this.buffer = this.buffer.replace(bufferMsg + this.delimiter, ""); + return bufferMsg; + } + return null + } + + returnData() { + return this.fetchBuffer(); + } + + clearStack() { + this.buffer = ""; + } + + readStackByDelimiter(delimiter) { + let backup = this.delimiter; + let part = ''; + this.delimiter = delimiter; + part = this.fetchBuffer(); + this.delimiter = backup; + return part; + } +} + function trim(s) { if (typeof s === 'string') return s.trim(); return s; @@ -982,7 +1024,7 @@ function populateEncounter(pd) { } function populateAllergy(pd) { - + if (!pd) { return { "no_know_allergies": "No Known Allergies", @@ -1654,13 +1696,13 @@ function getFunctionalStatus(pd) { let functionalStatusAuthor = { "code": { "name": all.author.physician_type || '', - "code": all.author.physician_type_code || '', - "code_system": all.author.physician_type_system, "code_system_name": all.author.physician_type_system_name + "code": all.author.physician_type_code || '', + "code_system": all.author.physician_type_system, "code_system_name": all.author.physician_type_system_name }, "date_time": { "point": { "date": authorDateTime, - "precision": "tz" + "precision": "tz" } }, "identifiers": [ @@ -1669,13 +1711,13 @@ function getFunctionalStatus(pd) { "extension": all.author.npi ? all.author.npi : '' } ], - "name": [ + "name": [ { "last": all.author.lname, "first": all.author.fname } ], - "organization": [ + "organization": [ { "identity": [ { @@ -1696,7 +1738,7 @@ function getFunctionalStatus(pd) { "identifiers": [{ "identifier": "9a6d1bac-17d3-4195-89a4-1121bc809000" }], - + "observation": { "value": { "name": pd.code_text !== "NULL" ? cleanText(pd.code_text) : "", @@ -2178,7 +2220,7 @@ function populateSocialHistory(pd) { } ] } - ,"gender_author": { + , "gender_author": { "code": { "name": all.patient.author.physician_type || '', "code": all.patient.author.physician_type_code || '', @@ -3244,14 +3286,14 @@ function genCcda(pd) { if (err) { return console.log(err); } - console.log("Json saved!"); + //console.log("Json saved!"); }); fs.writeFile(place + "ccda.xml", xml, function (err) { if (err) { return console.log(err); } - console.log("Xml saved!"); + //console.log("Xml saved!"); }); } } @@ -3267,22 +3309,19 @@ function processConnection(connection) { let xml_complete = ""; function eventData(xml) { - xml = xml.replace(/(\u000b|\u001c)/gm, "").trim(); - // Sanity check from service manager - if (xml === 'status' || xml.length < 80) { - conn.write("statusok" + String.fromCharCode(28) + "\r\r"); - conn.end(''); - return; - } - xml_complete += xml.toString(); - if (xml.toString().match(/<\/CCDA>$/g)) { - // ---------------------start-------------------------------- + xml_complete = xml.toString(); + //console.log("length: " + xml.length + " " + xml_complete); + // ensure we have an array start and end + if (xml_complete.match(/^$/g)) { let doc = ""; let xslUrl = ""; + xml_complete = xml_complete.replace(/(\u000b|\u001c)/gm, "").trim(); + // let's not allow windows CR/LF + xml_complete = xml_complete.replace(/[\r\n]/gm, '').trim(); xml_complete = xml_complete.replace(/\t\s+/g, ' ').trim(); // convert xml data set for document to json array to_json(xml_complete, function (error, data) { - // console.log(JSON.stringify(data, null, 4)); + //console.log(JSON.stringify(data, null, 4)); if (error) { // need try catch console.log('toJson error: ' + error + 'Len: ' + xml_complete.length); return; @@ -3296,11 +3335,10 @@ function processConnection(connection) { doc = headReplace(doc, xslUrl); doc = doc.toString().replace(/(\u000b|\u001c|\r)/gm, "").trim(); - //console.log(doc); let chunk = ""; let numChunks = Math.ceil(doc.length / 1024); for (let i = 0, o = 0; i < numChunks; ++i, o += 1024) { - chunk = doc.substr(o, 1024); + chunk = doc.substring(o, o + 1024); conn.write(chunk); } conn.write(String.fromCharCode(28) + "\r\r" + ''); @@ -3317,7 +3355,16 @@ function processConnection(connection) { } // Connection Events // - conn.on('data', eventData); + // CCM will send two File Separator characters to mark end of array. + let received = new DataStack(String.fromCharCode(28)); + conn.on("data", data => { + received.pushToStack(data); + while (!received.endOfCcda() && data.length > 0) { + data = ""; + eventData(received.returnData()) + } + }); + conn.once('close', eventCloseConn); conn.on('error', eventErrorConn); } @@ -3332,7 +3379,7 @@ function setUp(server) { // start up listener for requests from CCM or others. setUp(server); -/* For future use in header. Do not remove! */ +/* ---------------------------------For future use in header. Do not remove!-------------------------------------------- */ /*"data_enterer": { "identifiers": [ { diff --git a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CcdaServiceDocumentRequestor.php b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CcdaServiceDocumentRequestor.php index 702e66eb7..1f23b5d37 100644 --- a/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CcdaServiceDocumentRequestor.php +++ b/interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Model/CcdaServiceDocumentRequestor.php @@ -4,7 +4,7 @@ * CcdaServiceDocumentRequestor handles the communication with the node ccda service in sending and receiving data * over the socket. * - * @package openemr + * @package openemr * @link http://www.open-emr.org * @author Stephen Nielson * @copyright Copyright (c) 2022 Discover and Change @@ -28,7 +28,6 @@ class CcdaServiceDocumentRequestor if ($socket === false) { throw new CcdaServiceConnectionException("Socket Creation Failed"); } - // Let's check if server is already running but suppress warning with @ operator $server_active = @socket_connect($socket, "localhost", "6661"); @@ -74,31 +73,38 @@ class CcdaServiceDocumentRequestor throw new CcdaServiceConnectionException("Please Enable C-CDA Alternate Service in Global Settings"); } } - - $data = chr(11) . $data . chr(28) . "\r"; - if (strlen($data) > 1024 * 128) { - throw new CcdaServiceConnectionException("Export document exceeds the maximum size of 128KB"); - } - // Write to socket! - if (strlen($data) > 1024 * 64) { - $data1 = substr($data, 0, floor(strlen($data) / 2)); - $data2 = substr($data, floor(strlen($data) / 2)); - $out = socket_write($socket, $data1, strlen($data1)); - // give distance a chance to clear buffer - // we could handshake with a little effort - sleep(1); - $out = socket_write($socket, $data2, strlen($data2)); - } else { - $out = socket_write($socket, $data, strlen($data)); + // add file separator character for server end of message + $data = $data . chr(28) . chr(28); + $len = strlen($data); + // Set default buffer size to target data array size. + $good_buf = socket_set_option($socket, SOL_SOCKET, SO_SNDBUF, $len); + if ($good_buf === false) { // Can't set buffer + error_log("Failed to set socket buffer to " . $len); } + // make writeSize chunk either the size set above or the default buffer size (64Kb). + $writeSize = socket_get_option($socket, SOL_SOCKET, SO_SNDBUF); + $pos = 0; + $currentCounter = 0; + $maxLineAttempts = ($len / $writeSize) + 1; + do { + $line = substr($data, $pos, min($writeSize, $len - $pos)); + $out = socket_write($socket, $line, strlen($line)); + if ($out !== false) { + $pos += $out; // bytes written lets advance our position + } else { + break; + } + // pause for the receiving side + usleep(200000); + } while ($out !== false && $pos < $len && $currentCounter++ <= $maxLineAttempts); socket_set_nonblock($socket); - //Read from socket! + //Read back rendered document from node service! do { $line = ""; $line = trim(socket_read($socket, 1024, PHP_NORMAL_READ)); $output .= $line; - } while (!empty($line) && $line !== false); + } while (!empty($line)); $output = substr(trim($output), 0, strlen($output) - 1); // Close and return. -- 2.11.4.GIT