Skip to content

Commit

Permalink
feat: add ups cancel request API
Browse files Browse the repository at this point in the history
  • Loading branch information
Chinlinlee committed Jun 5, 2023
1 parent 7952464 commit 1d2c07a
Show file tree
Hide file tree
Showing 7 changed files with 237 additions and 3 deletions.
1 change: 1 addition & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ DICOM_STORE_ROOTPATH="/dicomFiles"
DICOMWEB_HOST="{host}"
DICOMWEB_PORT=8081
DICOMWEB_API="dicom-web"
DICOMWEB_AE="RACCOON"

# DICOM DIMSE
ENABLE_DIMSE=true
Expand Down
64 changes: 64 additions & 0 deletions api/dicom-web/controller/UPS-RS/cancel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* @description
* https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_11.12
* This transaction is used to stop the origin server from automatically subscribing the User-Agent to new Workitems. This does not delete any existing subscriptions to specific Workitems.
*/

const {
CancelWorkItemService
} = require("./service/cancel.service");
const { ApiLogger } = require("../../../../utils/logs/api-logger");
const { Controller } = require("../../../controller.class");
const { DicomWebServiceError } = require("@error/dicom-web-service");

class CancelWorkItemController extends Controller {
constructor(req, res) {
super(req, res);
}

async mainProcess() {
let apiLogger = new ApiLogger(this.request, "UPS-RS");

apiLogger.addTokenValue();
apiLogger.logger.info(`Cancel Work Item, params: ${this.paramsToString()}`);

try {
let service = new CancelWorkItemService(this.request, this.response);
await service.cancel();

return this.response
.set("Content-Type", "application/dicom+json")
.status(202)
.end();
} catch (e) {
let errorStr = JSON.stringify(e, Object.getOwnPropertyNames(e));
apiLogger.logger.error(errorStr);

if (e instanceof DicomWebServiceError) {
return this.response.status(e.code).json({
status: e.status,
message: e.message
});
}

this.response.writeHead(500, {
"Content-Type": "application/dicom+json"
});
this.response.end(JSON.stringify({
code: 500,
message: "An Server Exception Occurred"
}));
}
}
}

/**
*
* @param {import('express').Request} req
* @param {import('express').Response} res
*/
module.exports = async function (req, res) {
let controller = new CancelWorkItemController(req, res);

await controller.doPipeline();
};
108 changes: 108 additions & 0 deletions api/dicom-web/controller/UPS-RS/service/cancel.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
const _ = require("lodash");
const workItemModel = require("@models/mongodb/models/workItems");
const { DicomJsonModel, BaseDicomJson } = require("@models/DICOM/dicom-json-model");
const globalSubscriptionModel = require("@models/mongodb/models/upsGlobalSubscription");
const {
DicomWebServiceError,
DicomWebStatusCodes
} = require("@error/dicom-web-service");
const { BaseWorkItemService } = require("./base-workItem.service");
const { dictionary } = require("@models/DICOM/dicom-tags-dic");
const { UPS_EVENT_TYPE } = require("./workItem-event");
const { raccoonConfig } = require("@root/config-class");

class CancelWorkItemService extends BaseWorkItemService {

/**
*
* @param {import("express").Request} req
* @param {import("express").Response} res
*/
constructor(req, res) {
super(req, res);
this.upsInstanceUID = this.request.params.workItem;
this.workItem = null;
this.requestWorkItem = /** @type {Object[]} */(this.request.body).pop();
}

async cancel() {

this.workItem = await this.findOneWorkItem(this.upsInstanceUID);
let procedureStepState = this.workItem.getString(dictionary.keyword.ProcedureStepState);

if (procedureStepState === "IN PROGRESS") {
//Only send cancel info event now, IMO
//User need to watch for cancel event and use change state API to cancel
await this.addCancelEvent();
await this.triggerUpsEvents();
return;
} else if (procedureStepState === "CANCELED") {
this.response.set("Warning", "299 <service>: The UPS is already in the requested state of CANCELED.");
return this.response.status(200).json({
status: DicomWebStatusCodes.UPSAlreadyInRequestedStateOfCanceled,
message: "UPS Already In Requested State Of Canceled"
});
} else if (procedureStepState === "COMPLETED") {
throw new DicomWebServiceError(
DicomWebStatusCodes.UPSAlreadyCompleted,
"The request is inconsistent with the current state of the Target Workitem. The Target Workitem is in the COMPLETED state.",
409
);
} else if (procedureStepState === "SCHEDULED") {
throw new DicomWebServiceError(
DicomWebStatusCodes.UPSNotYetInProgress,
"The request is inconsistent with the current state of the Target Workitem. The Target Workitem is in the SCHEDULED state.",
409
);
}

}

/**
*
* @param {string} upsInstanceUID
* @returns
*/
async findOneWorkItem(upsInstanceUID) {

let workItem = await workItemModel.findOne({
upsInstanceUID: upsInstanceUID
});

if (!workItem) {
throw new DicomWebServiceError(
DicomWebStatusCodes.UPSDoesNotExist,
"The UPS instance not exist",
404
);
}

return new DicomJsonModel(workItem);

}

async addCancelEvent() {
let hitSubscriptions = await this.getHitSubscriptions(this.workItem);
let aeTitles = hitSubscriptions.map(v => v.aeTitle);

this.addUpsEvent(UPS_EVENT_TYPE.CancelRequested, this.upsInstanceUID, this.cancelRequestBy(), aeTitles);
}

cancelRequestBy() {
let eventInfo = new BaseDicomJson(
this.requestWorkItem,
dictionary.keyword.ReasonForCancellation,
dictionary.keyword.ProcedureStepDiscontinuationReasonCodeSequence,
dictionary.keyword.ContactURI,
dictionary.keyword.ContactDisplayName
);

eventInfo.setValue(dictionary.keyword.RequestingAE, raccoonConfig.dicomWebConfig.aeTitle);

return eventInfo.dicomJson;
}

}


module.exports.CancelWorkItemService = CancelWorkItemService;
4 changes: 4 additions & 0 deletions api/dicom-web/ups-rs.route.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,10 @@ router.post("/workitems/:workItem/subscribers/:subscriberAeTitle/suspend",
require("./controller/UPS-RS/suspend-subscription")
);

router.post("/workitems/:workItem/cancelrequest",
require("./controller/UPS-RS/cancel")
);

//#endregion

module.exports = router;
2 changes: 1 addition & 1 deletion config-class.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class DicomWebConfig {
this.host = env.get("DICOMWEB_HOST").default("127.0.0.1").asString();
this.port = env.get("DICOMWEB_PORT").default("8081").asInt();
this.apiPath = env.get("DICOMWEB_API").default("dicom-web").asString();

this.aeTitle = env.get("DICOMWEB_AE").default("RACCOON").asString();
}
}

Expand Down
5 changes: 4 additions & 1 deletion error/dicom-web-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ const DicomWebStatusCodes = {
"MissingAttribute": "0120",
"ProcessingFailure": "0272",
"NoSuchObjectInstance": "0274",
"UPSAlreadyInRequestedStateOfCanceled": "B304",
"UPSMayNoLongerBeUpdated": "C300",
"UPSTransactionUIDNotCorrect": "C301",
"UPSAlreadyInProgress": "C302",
"UPSNotMetFinalStateRequirements": "C304",
"UPSDoesNotExist": "C307",
"UPSUnknownReceivingAET": "C308",
"UPSNotScheduled": "C309",
"UPSNotYetInProgress": "C310"
"UPSNotYetInProgress": "C310",
"UPSAlreadyCompleted": "C311",
"UPSPerformerChoosesNotToCancel": "C313"
};

class DicomWebServiceError extends Error {
Expand Down
56 changes: 55 additions & 1 deletion models/DICOM/dicom-json-model.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const patientModel = require("../mongodb/models/patient");
const { tagsNeedStore } = require("./dicom-tags-mapping");

const { raccoonConfig } = require("../../config-class");
const { JSONPath } = require("jsonpath-plus");
const { dictionary } = require("./dicom-tags-dic");

/**
* *The SQ of Whole slide may have over thousand of length cause process block.
Expand All @@ -25,6 +27,57 @@ const BIG_VALUE_TAGS = [
"00480200"
];

class BaseDicomJson {
/**
*
* @param {Object} dicomJson
* @param {...string} selection
*/
constructor(dicomJson, ...selection) {
this.dicomJson = dicomJson;
if(selection.length > 0) {
this.initSelectionJson(selection);
}
}

/**
*
* @param {string[]} selection
*/
initSelectionJson(selection) {
let selectionJson = {};
for(let i = 0; i < selection.length; i++) {
let tag = selection[i];
let item = this.getItem(tag);
if (item) {
_.set(selectionJson, tag, item);
}
}
this.dicomJson = selectionJson;
}

getItem(tag) {
return _.get(this.dicomJson, tag, undefined);
}

getValue(tag) {
return _.get(this.dicomJson, `${tag}.Value`, undefined);
}

getSequenceItem(tag) {
return JSONPath({
path: `$..${tag}`,
json: this.dicomJson
});
}

setValue(tag, value) {
let vrOfTag = _.get(dictionary.tagVR, `${tag}.vr`);
_.set(this.dicomJson, `${tag}.vr`, vrOfTag);
_.set(this.dicomJson, `${tag}.Value`, [value]);
}
}

class DicomJsonModel {
constructor(dicomJson) {
this.dicomJson = dicomJson;
Expand Down Expand Up @@ -512,4 +565,5 @@ class BulkData {
}

module.exports.DicomJsonModel = DicomJsonModel;
module.exports.DicomJsonBinaryDataModel = DicomJsonBinaryDataModel;
module.exports.DicomJsonBinaryDataModel = DicomJsonBinaryDataModel;
module.exports.BaseDicomJson = BaseDicomJson;

0 comments on commit 1d2c07a

Please sign in to comment.