Skip to content

Commit

Permalink
feat: C-Move DIMSE service
Browse files Browse the repository at this point in the history
  • Loading branch information
Chinlinlee committed Aug 21, 2023
1 parent 941408b commit 038d0a2
Show file tree
Hide file tree
Showing 170 changed files with 8,695 additions and 4,994 deletions.
9 changes: 9 additions & 0 deletions config/ae-prod.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Configure Host, Port, Cipher Suites for Move Destinations and Storage Commitment SCUs
# Format:
# <ae-title>=<hostname>:<port>[:cipher1[:...]]

STORESCP=localhost:11113
STORESCP_TLS=localhost:2762:SSL_RSA_WITH_NULL_SHA:TLS_RSA_WITH_AES_128_CBC_SHA:TLS_RSA_WITH_3DES_EDE_CBC_SHA
STGCMTSCU=localhost:11115
STGCMTSCU_TLS=localhost:12762:SSL_RSA_WITH_NULL_SHA:TLS_RSA_WITH_AES_128_CBC_SHA:TLS_RSA_WITH_3DES_EDE_CBC_SHA
MOVESCU=localhost:1234
186 changes: 186 additions & 0 deletions dimse/c-move.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
const _ = require("lodash");
const path = require("path");
const { mongoose } = require("mongoose");

const { Attributes } = require("@dcm4che/data/Attributes");
const { Tag } = require("@dcm4che/data/Tag");
const { Association } = require("@dcm4che/net/Association");
const { Status } = require("@dcm4che/net/Status");
const { PresentationContext } = require("@dcm4che/net/pdu/PresentationContext");
const { DicomServiceError } = require("@error/dicom-service");
const { createCMoveSCPInjectProxy } = require("@java-wrapper/org/github/chinlinlee/dcm777/net/CMoveSCPInject");
const { DimseQueryBuilder } = require("./queryBuilder");
const { File } = require("@java-wrapper/java/io/File");
const { raccoonConfig } = require("@root/config-class");
const { SimpleCMoveSCP } = require("@java-wrapper/org/github/chinlinlee/dcm777/net/SimpleCMoveSCP");
const { UID } = require("@dcm4che/data/UID");

const { PATIENT_ROOT_LEVELS, STUDY_ROOT_LEVELS, PATIENT_STUDY_ONLY_LEVELS } = require("./level");
const { importClass } = require("java-bridge");
const { default: InstanceLocator } = require("@dcm4che/net/service/InstanceLocator");
const { default: AAssociateRQ } = require("@dcm4che/net/pdu/AAssociateRQ");
const { default: Connection } = require("@dcm4che/net/Connection");
const { default: RetrieveTaskImpl } = require("@dcm4che/tool/dcmqrscp/RetrieveTaskImpl");
const { Dimse } = require("@dcm4che/net/Dimse");

class JsCMoveScp {
constructor(dcmQrScp) {
/** @type { import("./index").DcmQrScp } */
this.dcmQrScp = dcmQrScp;
}

getPatientRootLevel() {
const cMoveScpInject = createCMoveSCPInjectProxy(this.getCMoveScpInjectProxyMethods(), {
keepAsDaemon: true
});

return new SimpleCMoveSCP(
cMoveScpInject,
UID.PatientRootQueryRetrieveInformationModelMove,
PATIENT_ROOT_LEVELS
);
}

getCMoveScpInjectProxyMethods() {
/** @type { import("@java-wrapper/org/github/chinlinlee/dcm777/net/CMoveSCPInject").CMoveSCPInjectInterface } */
const cMoveScpInjectProxyMethods = {
/**
*
* @param {Association} as
* @param {PresentationContext} pc
* @param {Attributes} rq
* @param {Attributes} keys
*/
calculateMatches: async (as, pc, rq, keys) => {
try {
let moveDest = await rq.getString(Tag.MoveDestination);
const remote = this.dcmQrScp.getRemoteConnection(moveDest);
if (!remote) {
throw new DicomServiceError(Status.MoveDestinationUnknown, `Move Destination: ${moveDest} unknown`);
}

let instances = await this.getInstances(keys);
if (await instances.isEmpty()) {
return null;
}

let aAssociateRq = await this.makeAAssociateRQ_(await as.getLocalAET(), moveDest, instances);
let storeAssociation = await this.openStoreAssociation_(as, remote, aAssociateRq);
let retrieveTask = await RetrieveTaskImpl.newInstanceAsync(
Dimse.C_MOVE_RQ,
as,
pc,
rq,
instances,
storeAssociation,
false,
0
);
await retrieveTask.setSendPendingRSPInterval(0);
return retrieveTask;
} catch (e) {
console.error(e);
throw e;
}
}
};

return cMoveScpInjectProxyMethods;
}

async getInstances(keys) {
let queryBuilder = new DimseQueryBuilder(keys, "instance");
let normalQuery = await queryBuilder.toNormalQuery();
let mongoQuery = await queryBuilder.getMongoQuery(normalQuery);

let returnKeys = {
"instancePath": 1,
"00020010": 1,
"00080016": 1,
"00080018": 1,
"0020000D": 1,
"0020000E": 1
};

let instances = await mongoose.model("dicom").find({
...mongoQuery.$match
}, returnKeys).setOptions({
strictQuery: false
}).exec();
const JArrayList = await importClass("java.util.ArrayList");
let list = await JArrayList.newInstanceAsync();

for (let instance of instances) {
let instanceFile = await File.newInstanceAsync(
path.join(
raccoonConfig.dicomWebConfig.storeRootPath,
instance.instancePath
)
);

let fileUri = await instanceFile.toURI();
let fileUriString = await fileUri.toString();

let instanceLocator = await InstanceLocator.newInstanceAsync(
_.get(instance, "00080016.Value.0"),
_.get(instance, "00080018.Value.0"),
_.get(instance, "00020010.Value.0"),
fileUriString
);

await list.add(instanceLocator);
}

return list;
}

/**
* @private
* @param {string} callingAet
* @param {string} calledAet
* @param {*} matches
*/
async makeAAssociateRQ_(callingAet, calledAet, matches) {
let aAssociateRq = await AAssociateRQ.newInstanceAsync();
await aAssociateRq.setCallingAET(callingAet);
await aAssociateRq.setCalledAET(calledAet);
let matchesArray = await matches.toArray();
for (let match of matchesArray) {
if (await aAssociateRq.addPresentationContextFor(match.cuid, match.tsuid)) {

if (!UID.ExplicitVRLittleEndian === match.tsuid) {
await aAssociateRq.addPresentationContextFor(match.cuid, UID.ExplicitVRLittleEndian);
}

if (!UID.ImplicitVRLittleEndian === match.tsuid) {
await aAssociateRq.addPresentationContextFor(match.cuid, UID.ImplicitVRLittleEndian);
}
}
}
return aAssociateRq;
}

/**
* @private
* @param {Association} as
* @param {Connection} remote
* @param {AAssociateRQ} aAssociateRq
* @returns
*/
async openStoreAssociation_(as, remote, aAssociateRq) {
try {
let applicationEntity = await as.getApplicationEntity();

return await applicationEntity.connect(
await as.getConnection(),
remote,
aAssociateRq
);

} catch (e) {
throw new DicomServiceError(Status.UnableToPerformSubOperations, e);
}
}
}

module.exports.JsCMoveScp = JsCMoveScp;
65 changes: 64 additions & 1 deletion dimse/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require("module-alias/register");
const _ = require("lodash");
const { java } = require("@models/DICOM/dcm4che/java-instance");
const { importClass, appendClasspath, stdout, newProxy } = require("java-bridge");
const glob = require("glob");
Expand All @@ -15,6 +16,9 @@ const { TransferCapability } = require("@dcm4che/net/TransferCapability");
const { TransferCapability$Role: TransferCapabilityRole } = require("@dcm4che/net/TransferCapability$Role");
const { JsCStoreScp } = require("./c-store");
const { JsCFindScp } = require("./c-find");
const { default: CLIUtils } = require("@dcm4che/tool/common/CLIUtils");
const { JsCMoveScp } = require("./c-move");
const fileExist = require("@root/utils/file/fileExist");

const aeTitle = "FKQRSCP";
const host = "0.0.0.0";
Expand All @@ -24,6 +28,7 @@ class DcmQrScp {
device = new Device("dcmqrscp");
applicationEntity = new ApplicationEntity("*");
connection = new Connection();
remoteConnections = {};

constructor() {
this.device.addConnectionSync(this.connection);
Expand All @@ -41,15 +46,20 @@ class DcmQrScp {
await dicomServiceRegistry.addDicomService(new BasicCEchoSCP());
let jsCStoreScp = new JsCStoreScp();
await dicomServiceRegistry.addDicomService(jsCStoreScp.get());

await dicomServiceRegistry.addDicomService(new JsCFindScp().getPatientRootLevel());
await dicomServiceRegistry.addDicomService(new JsCFindScp().getStudyRootLevel());
await dicomServiceRegistry.addDicomService(new JsCFindScp().getPatientStudyOnlyLevel());

await dicomServiceRegistry.addDicomService(new JsCMoveScp(this).getPatientRootLevel());
return dicomServiceRegistry;
}


async start() {
this.configureBindServer();
this.configureTransferCapability();
this.configureRemoteConnections();


const Executors = importClass("java.util.concurrent.Executors");
Expand Down Expand Up @@ -79,10 +89,63 @@ class DcmQrScp {
this.applicationEntity.setAETitleSync(aeTitle);
}

configureRemoteConnections() {
let aeFile = path.normalize(
path.join(
__dirname,
"../config/ae-prod.properties"
)
);
if (!fileExist.sync(aeFile)) {
aeFile = path.normalize(
path.join(
__dirname,
"../config/ae.properties"
)
);
}

let aeConfig = CLIUtils.loadPropertiesSync(aeFile, null);
let itemsSet = aeConfig.entrySetSync();
let itemsIter = itemsSet.iteratorSync();

let item;
while(itemsIter.hasNextSync()) {
item = itemsIter.nextSync();
/** @type {string} */
let aet = item.getKeySync();
/** @type {string} */
let value = item.getValueSync();
try {
let hostPortCiphers = value.split(":");
let ciphers = hostPortCiphers.slice(2);

let remote = new Connection();
remote.setHostnameSync(hostPortCiphers[0]);
remote.setPortSync(parseInt(hostPortCiphers[1]));
remote.setTlsCipherSuitesSync(ciphers);
this.remoteConnections[aet] = remote;
} catch(e) {
console.error(e);
throw new (`Invalid entry in ${file}: ${aet}=${value}`);
}
}
}

/**
* @param {string} dest
*/
getRemoteConnection(dest) {
return _.get(this.remoteConnections, dest);
}

}


process.stdin.resume();

let dcmQrScp = new DcmQrScp();
dcmQrScp.start();
dcmQrScp.start();


module.exports.DcmQrScp = DcmQrScp;
11 changes: 11 additions & 0 deletions error/dicom-service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class DicomServiceError extends Error {
constructor(status, message) {
super(message);
Error.captureStackTrace(this, this.constructor);

this.name = this.constructor.name;
this.status = status;
}
}

module.exports.DicomServiceError = DicomServiceError;
Binary file not shown.
16 changes: 8 additions & 8 deletions models/DICOM/dcm4che/wrapper/java/io/Reader.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,6 @@ export declare class ReaderClass extends JavaClass {
* @return original return type: 'void'
*/
resetSync(): void;
/**
* @return original return type: 'java.io.Reader'
*/
static nullReader(): Promise<Reader | null>;
/**
* @return original return type: 'java.io.Reader'
*/
static nullReaderSync(): Reader | null;
/**
* @return original return type: 'boolean'
*/
Expand All @@ -122,6 +114,14 @@ export declare class ReaderClass extends JavaClass {
* @return original return type: 'boolean'
*/
readySync(): boolean;
/**
* @return original return type: 'java.io.Reader'
*/
static nullReader(): Promise<Reader | null>;
/**
* @return original return type: 'java.io.Reader'
*/
static nullReaderSync(): Reader | null;
/**
* @param var0 original type: 'long'
* @param var1 original type: 'int'
Expand Down
2 changes: 1 addition & 1 deletion models/DICOM/dcm4che/wrapper/java/io/Reader.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 038d0a2

Please sign in to comment.