comparison Core/DicomNetworking/DicomControlUserConnection.cpp @ 3825:4570c57668a8

refactoring DicomUserConnection as Dicom[Control|Store]UserConnection
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 10 Apr 2020 16:04:54 +0200
parents
children e82bd07c384e
comparison
equal deleted inserted replaced
3817:37e20bbf25f5 3825:4570c57668a8
1 /**
2 * Orthanc - A Lightweight, RESTful DICOM Store
3 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
4 * Department, University Hospital of Liege, Belgium
5 * Copyright (C) 2017-2020 Osimis S.A., Belgium
6 *
7 * This program is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License as
9 * published by the Free Software Foundation, either version 3 of the
10 * License, or (at your option) any later version.
11 *
12 * In addition, as a special exception, the copyright holders of this
13 * program give permission to link the code of its release with the
14 * OpenSSL project's "OpenSSL" library (or with modified versions of it
15 * that use the same license as the "OpenSSL" library), and distribute
16 * the linked executables. You must obey the GNU General Public License
17 * in all respects for all of the code used other than "OpenSSL". If you
18 * modify file(s) with this exception, you may extend this exception to
19 * your version of the file(s), but you are not obligated to do so. If
20 * you do not wish to do so, delete this exception statement from your
21 * version. If you delete this exception statement from all source files
22 * in the program, then also delete it here.
23 *
24 * This program is distributed in the hope that it will be useful, but
25 * WITHOUT ANY WARRANTY; without even the implied warranty of
26 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
27 * General Public License for more details.
28 *
29 * You should have received a copy of the GNU General Public License
30 * along with this program. If not, see <http://www.gnu.org/licenses/>.
31 **/
32
33
34 #include "../PrecompiledHeaders.h"
35 #include "DicomControlUserConnection.h"
36
37 #include "../Logging.h"
38 #include "../OrthancException.h"
39 #include "../DicomParsing/FromDcmtkBridge.h"
40
41 #include <dcmtk/dcmdata/dcdeftag.h>
42 #include <dcmtk/dcmnet/diutil.h>
43
44 namespace Orthanc
45 {
46 static void TestAndCopyTag(DicomMap& result,
47 const DicomMap& source,
48 const DicomTag& tag)
49 {
50 if (!source.HasTag(tag))
51 {
52 throw OrthancException(ErrorCode_BadRequest);
53 }
54 else
55 {
56 result.SetValue(tag, source.GetValue(tag));
57 }
58 }
59
60
61 namespace
62 {
63 struct FindPayload
64 {
65 DicomFindAnswers* answers;
66 const char* level;
67 bool isWorklist;
68 };
69 }
70
71
72 static void FindCallback(
73 /* in */
74 void *callbackData,
75 T_DIMSE_C_FindRQ *request, /* original find request */
76 int responseCount,
77 T_DIMSE_C_FindRSP *response, /* pending response received */
78 DcmDataset *responseIdentifiers /* pending response identifiers */
79 )
80 {
81 FindPayload& payload = *reinterpret_cast<FindPayload*>(callbackData);
82
83 if (responseIdentifiers != NULL)
84 {
85 if (payload.isWorklist)
86 {
87 ParsedDicomFile answer(*responseIdentifiers);
88 payload.answers->Add(answer);
89 }
90 else
91 {
92 DicomMap m;
93 FromDcmtkBridge::ExtractDicomSummary(m, *responseIdentifiers);
94
95 if (!m.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL))
96 {
97 m.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, payload.level, false);
98 }
99
100 payload.answers->Add(m);
101 }
102 }
103 }
104
105
106 static void NormalizeFindQuery(DicomMap& fixedQuery,
107 ResourceType level,
108 const DicomMap& fields)
109 {
110 std::set<DicomTag> allowedTags;
111
112 // WARNING: Do not add "break" or reorder items in this switch-case!
113 switch (level)
114 {
115 case ResourceType_Instance:
116 DicomTag::AddTagsForModule(allowedTags, DicomModule_Instance);
117
118 case ResourceType_Series:
119 DicomTag::AddTagsForModule(allowedTags, DicomModule_Series);
120
121 case ResourceType_Study:
122 DicomTag::AddTagsForModule(allowedTags, DicomModule_Study);
123
124 case ResourceType_Patient:
125 DicomTag::AddTagsForModule(allowedTags, DicomModule_Patient);
126 break;
127
128 default:
129 throw OrthancException(ErrorCode_InternalError);
130 }
131
132 switch (level)
133 {
134 case ResourceType_Patient:
135 allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES);
136 allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES);
137 allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES);
138 break;
139
140 case ResourceType_Study:
141 allowedTags.insert(DICOM_TAG_MODALITIES_IN_STUDY);
142 allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES);
143 allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES);
144 allowedTags.insert(DICOM_TAG_SOP_CLASSES_IN_STUDY);
145 break;
146
147 case ResourceType_Series:
148 allowedTags.insert(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES);
149 break;
150
151 default:
152 break;
153 }
154
155 allowedTags.insert(DICOM_TAG_SPECIFIC_CHARACTER_SET);
156
157 DicomArray query(fields);
158 for (size_t i = 0; i < query.GetSize(); i++)
159 {
160 const DicomTag& tag = query.GetElement(i).GetTag();
161 if (allowedTags.find(tag) == allowedTags.end())
162 {
163 LOG(WARNING) << "Tag not allowed for this C-Find level, will be ignored: " << tag;
164 }
165 else
166 {
167 fixedQuery.SetValue(tag, query.GetElement(i).GetValue());
168 }
169 }
170 }
171
172
173
174 static ParsedDicomFile* ConvertQueryFields(const DicomMap& fields,
175 ModalityManufacturer manufacturer)
176 {
177 // Fix outgoing C-Find requests issue for Syngo.Via and its
178 // solution was reported by Emsy Chan by private mail on
179 // 2015-06-17. According to Robert van Ommen (2015-11-30), the
180 // same fix is required for Agfa Impax. This was generalized for
181 // generic manufacturer since it seems to affect PhilipsADW,
182 // GEWAServer as well:
183 // https://bitbucket.org/sjodogne/orthanc/issues/31/
184
185 switch (manufacturer)
186 {
187 case ModalityManufacturer_GenericNoWildcardInDates:
188 case ModalityManufacturer_GenericNoUniversalWildcard:
189 {
190 std::unique_ptr<DicomMap> fix(fields.Clone());
191
192 std::set<DicomTag> tags;
193 fix->GetTags(tags);
194
195 for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
196 {
197 // Replace a "*" wildcard query by an empty query ("") for
198 // "date" or "all" value representations depending on the
199 // type of manufacturer.
200 if (manufacturer == ModalityManufacturer_GenericNoUniversalWildcard ||
201 (manufacturer == ModalityManufacturer_GenericNoWildcardInDates &&
202 FromDcmtkBridge::LookupValueRepresentation(*it) == ValueRepresentation_Date))
203 {
204 const DicomValue* value = fix->TestAndGetValue(*it);
205
206 if (value != NULL &&
207 !value->IsNull() &&
208 value->GetContent() == "*")
209 {
210 fix->SetValue(*it, "", false);
211 }
212 }
213 }
214
215 return new ParsedDicomFile(*fix, GetDefaultDicomEncoding(), false /* be strict */);
216 }
217
218 default:
219 return new ParsedDicomFile(fields, GetDefaultDicomEncoding(), false /* be strict */);
220 }
221 }
222
223
224
225 void DicomControlUserConnection::SetupPresentationContexts()
226 {
227 association_.ProposeGenericPresentationContext(UID_VerificationSOPClass);
228 association_.ProposeGenericPresentationContext(UID_FINDPatientRootQueryRetrieveInformationModel);
229 association_.ProposeGenericPresentationContext(UID_FINDStudyRootQueryRetrieveInformationModel);
230 association_.ProposeGenericPresentationContext(UID_MOVEStudyRootQueryRetrieveInformationModel);
231 association_.ProposeGenericPresentationContext(UID_FINDModalityWorklistInformationModel);
232 }
233
234
235 void DicomControlUserConnection::FindInternal(DicomFindAnswers& answers,
236 DcmDataset* dataset,
237 const char* sopClass,
238 bool isWorklist,
239 const char* level)
240 {
241 assert(isWorklist ^ (level != NULL));
242
243 association_.Open(parameters_);
244
245 FindPayload payload;
246 payload.answers = &answers;
247 payload.level = level;
248 payload.isWorklist = isWorklist;
249
250 // Figure out which of the accepted presentation contexts should be used
251 int presID = ASC_findAcceptedPresentationContextID(
252 &association_.GetDcmtkAssociation(), sopClass);
253 if (presID == 0)
254 {
255 throw OrthancException(ErrorCode_DicomFindUnavailable,
256 "Remote AET is " + parameters_.GetRemoteApplicationEntityTitle());
257 }
258
259 T_DIMSE_C_FindRQ request;
260 memset(&request, 0, sizeof(request));
261 request.MessageID = association_.GetDcmtkAssociation().nextMsgID++;
262 strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN);
263 request.Priority = DIMSE_PRIORITY_MEDIUM;
264 request.DataSetType = DIMSE_DATASET_PRESENT;
265
266 T_DIMSE_C_FindRSP response;
267 DcmDataset* statusDetail = NULL;
268
269 #if DCMTK_VERSION_NUMBER >= 364
270 int responseCount;
271 #endif
272
273 OFCondition cond = DIMSE_findUser(
274 &association_.GetDcmtkAssociation(), presID, &request, dataset,
275 #if DCMTK_VERSION_NUMBER >= 364
276 responseCount,
277 #endif
278 FindCallback, &payload,
279 /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
280 /*opt_dimse_timeout*/ parameters_.GetTimeout(),
281 &response, &statusDetail);
282
283 if (statusDetail)
284 {
285 delete statusDetail;
286 }
287
288 DicomAssociation::CheckCondition(cond, parameters_, "C-FIND");
289
290
291 /**
292 * New in Orthanc 1.6.0: Deal with failures during C-FIND.
293 * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.html#table_C.4-1
294 **/
295
296 if (response.DimseStatus != 0x0000 && // Success
297 response.DimseStatus != 0xFF00 && // Pending - Matches are continuing
298 response.DimseStatus != 0xFF01) // Pending - Matches are continuing
299 {
300 char buf[16];
301 sprintf(buf, "%04X", response.DimseStatus);
302
303 if (response.DimseStatus == STATUS_FIND_Failed_UnableToProcess)
304 {
305 throw OrthancException(ErrorCode_NetworkProtocol,
306 HttpStatus_422_UnprocessableEntity,
307 "C-FIND SCU to AET \"" +
308 parameters_.GetRemoteApplicationEntityTitle() +
309 "\" has failed with DIMSE status 0x" + buf +
310 " (unable to process - invalid query ?)");
311 }
312 else
313 {
314 throw OrthancException(ErrorCode_NetworkProtocol, "C-FIND SCU to AET \"" +
315 parameters_.GetRemoteApplicationEntityTitle() +
316 "\" has failed with DIMSE status 0x" + buf);
317 }
318 }
319 }
320
321
322 void DicomControlUserConnection::MoveInternal(const std::string& targetAet,
323 ResourceType level,
324 const DicomMap& fields)
325 {
326 association_.Open(parameters_);
327
328 std::unique_ptr<ParsedDicomFile> query(
329 ConvertQueryFields(fields, parameters_.GetRemoteManufacturer()));
330 DcmDataset* dataset = query->GetDcmtkObject().getDataset();
331
332 const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel;
333 switch (level)
334 {
335 case ResourceType_Patient:
336 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "PATIENT");
337 break;
338
339 case ResourceType_Study:
340 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "STUDY");
341 break;
342
343 case ResourceType_Series:
344 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES");
345 break;
346
347 case ResourceType_Instance:
348 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE");
349 break;
350
351 default:
352 throw OrthancException(ErrorCode_ParameterOutOfRange);
353 }
354
355 // Figure out which of the accepted presentation contexts should be used
356 int presID = ASC_findAcceptedPresentationContextID(&association_.GetDcmtkAssociation(), sopClass);
357 if (presID == 0)
358 {
359 throw OrthancException(ErrorCode_DicomMoveUnavailable,
360 "Remote AET is " + parameters_.GetRemoteApplicationEntityTitle());
361 }
362
363 T_DIMSE_C_MoveRQ request;
364 memset(&request, 0, sizeof(request));
365 request.MessageID = association_.GetDcmtkAssociation().nextMsgID++;
366 strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN);
367 request.Priority = DIMSE_PRIORITY_MEDIUM;
368 request.DataSetType = DIMSE_DATASET_PRESENT;
369 strncpy(request.MoveDestination, targetAet.c_str(), DIC_AE_LEN);
370
371 T_DIMSE_C_MoveRSP response;
372 DcmDataset* statusDetail = NULL;
373 DcmDataset* responseIdentifiers = NULL;
374 OFCondition cond = DIMSE_moveUser(
375 &association_.GetDcmtkAssociation(), presID, &request, dataset, NULL, NULL,
376 /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
377 /*opt_dimse_timeout*/ parameters_.GetTimeout(),
378 &association_.GetDcmtkNetwork(), NULL, NULL,
379 &response, &statusDetail, &responseIdentifiers);
380
381 if (statusDetail)
382 {
383 delete statusDetail;
384 }
385
386 if (responseIdentifiers)
387 {
388 delete responseIdentifiers;
389 }
390
391 DicomAssociation::CheckCondition(cond, parameters_, "C-MOVE");
392
393
394 /**
395 * New in Orthanc 1.6.0: Deal with failures during C-MOVE.
396 * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.2.html#table_C.4-2
397 **/
398
399 if (response.DimseStatus != 0x0000 && // Success
400 response.DimseStatus != 0xFF00) // Pending - Sub-operations are continuing
401 {
402 char buf[16];
403 sprintf(buf, "%04X", response.DimseStatus);
404
405 if (response.DimseStatus == STATUS_MOVE_Failed_UnableToProcess)
406 {
407 throw OrthancException(ErrorCode_NetworkProtocol,
408 HttpStatus_422_UnprocessableEntity,
409 "C-MOVE SCU to AET \"" +
410 parameters_.GetRemoteApplicationEntityTitle() +
411 "\" has failed with DIMSE status 0x" + buf +
412 " (unable to process - resource not found ?)");
413 }
414 else
415 {
416 throw OrthancException(ErrorCode_NetworkProtocol, "C-MOVE SCU to AET \"" +
417 parameters_.GetRemoteApplicationEntityTitle() +
418 "\" has failed with DIMSE status 0x" + buf);
419 }
420 }
421 }
422
423
424 DicomControlUserConnection::DicomControlUserConnection(const DicomAssociationParameters& params) :
425 parameters_(params)
426 {
427 SetupPresentationContexts();
428 }
429
430
431 bool DicomControlUserConnection::Echo()
432 {
433 association_.Open(parameters_);
434
435 DIC_US status;
436 DicomAssociation::CheckCondition(
437 DIMSE_echoUser(&association_.GetDcmtkAssociation(),
438 association_.GetDcmtkAssociation().nextMsgID++,
439 /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
440 /*opt_dimse_timeout*/ parameters_.GetTimeout(),
441 &status, NULL),
442 parameters_, "C-ECHO");
443
444 return status == STATUS_Success;
445 }
446
447
448 void DicomControlUserConnection::Find(DicomFindAnswers& result,
449 ResourceType level,
450 const DicomMap& originalFields,
451 bool normalize)
452 {
453 std::unique_ptr<ParsedDicomFile> query;
454
455 if (normalize)
456 {
457 DicomMap fields;
458 NormalizeFindQuery(fields, level, originalFields);
459 query.reset(ConvertQueryFields(fields, parameters_.GetRemoteManufacturer()));
460 }
461 else
462 {
463 query.reset(new ParsedDicomFile(originalFields,
464 GetDefaultDicomEncoding(),
465 false /* be strict */));
466 }
467
468 DcmDataset* dataset = query->GetDcmtkObject().getDataset();
469
470 const char* clevel = NULL;
471 const char* sopClass = NULL;
472
473 switch (level)
474 {
475 case ResourceType_Patient:
476 clevel = "PATIENT";
477 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "PATIENT");
478 sopClass = UID_FINDPatientRootQueryRetrieveInformationModel;
479 break;
480
481 case ResourceType_Study:
482 clevel = "STUDY";
483 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "STUDY");
484 sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
485 break;
486
487 case ResourceType_Series:
488 clevel = "SERIES";
489 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES");
490 sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
491 break;
492
493 case ResourceType_Instance:
494 clevel = "IMAGE";
495 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE");
496 sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
497 break;
498
499 default:
500 throw OrthancException(ErrorCode_ParameterOutOfRange);
501 }
502
503
504 const char* universal;
505 if (parameters_.GetRemoteManufacturer() == ModalityManufacturer_GE)
506 {
507 universal = "*";
508 }
509 else
510 {
511 universal = "";
512 }
513
514
515 // Add the expected tags for this query level.
516 // WARNING: Do not reorder or add "break" in this switch-case!
517 switch (level)
518 {
519 case ResourceType_Instance:
520 if (!dataset->tagExists(DCM_SOPInstanceUID))
521 {
522 DU_putStringDOElement(dataset, DCM_SOPInstanceUID, universal);
523 }
524
525 case ResourceType_Series:
526 if (!dataset->tagExists(DCM_SeriesInstanceUID))
527 {
528 DU_putStringDOElement(dataset, DCM_SeriesInstanceUID, universal);
529 }
530
531 case ResourceType_Study:
532 if (!dataset->tagExists(DCM_AccessionNumber))
533 {
534 DU_putStringDOElement(dataset, DCM_AccessionNumber, universal);
535 }
536
537 if (!dataset->tagExists(DCM_StudyInstanceUID))
538 {
539 DU_putStringDOElement(dataset, DCM_StudyInstanceUID, universal);
540 }
541
542 case ResourceType_Patient:
543 if (!dataset->tagExists(DCM_PatientID))
544 {
545 DU_putStringDOElement(dataset, DCM_PatientID, universal);
546 }
547
548 break;
549
550 default:
551 throw OrthancException(ErrorCode_ParameterOutOfRange);
552 }
553
554 assert(clevel != NULL && sopClass != NULL);
555 FindInternal(result, dataset, sopClass, false, clevel);
556 }
557
558
559 void DicomControlUserConnection::Move(const std::string& targetAet,
560 ResourceType level,
561 const DicomMap& findResult)
562 {
563 DicomMap move;
564 switch (level)
565 {
566 case ResourceType_Patient:
567 TestAndCopyTag(move, findResult, DICOM_TAG_PATIENT_ID);
568 break;
569
570 case ResourceType_Study:
571 TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
572 break;
573
574 case ResourceType_Series:
575 TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
576 TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID);
577 break;
578
579 case ResourceType_Instance:
580 TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
581 TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID);
582 TestAndCopyTag(move, findResult, DICOM_TAG_SOP_INSTANCE_UID);
583 break;
584
585 default:
586 throw OrthancException(ErrorCode_InternalError);
587 }
588
589 MoveInternal(targetAet, level, move);
590 }
591
592
593 void DicomControlUserConnection::Move(const std::string& targetAet,
594 const DicomMap& findResult)
595 {
596 if (!findResult.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL))
597 {
598 throw OrthancException(ErrorCode_InternalError);
599 }
600
601 const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent();
602 ResourceType level = StringToResourceType(tmp.c_str());
603
604 Move(targetAet, level, findResult);
605 }
606
607
608 void DicomControlUserConnection::MovePatient(const std::string& targetAet,
609 const std::string& patientId)
610 {
611 DicomMap query;
612 query.SetValue(DICOM_TAG_PATIENT_ID, patientId, false);
613 MoveInternal(targetAet, ResourceType_Patient, query);
614 }
615
616
617 void DicomControlUserConnection::MoveStudy(const std::string& targetAet,
618 const std::string& studyUid)
619 {
620 DicomMap query;
621 query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false);
622 MoveInternal(targetAet, ResourceType_Study, query);
623 }
624
625
626 void DicomControlUserConnection::MoveSeries(const std::string& targetAet,
627 const std::string& studyUid,
628 const std::string& seriesUid)
629 {
630 DicomMap query;
631 query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false);
632 query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false);
633 MoveInternal(targetAet, ResourceType_Series, query);
634 }
635
636
637 void DicomControlUserConnection::MoveInstance(const std::string& targetAet,
638 const std::string& studyUid,
639 const std::string& seriesUid,
640 const std::string& instanceUid)
641 {
642 DicomMap query;
643 query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false);
644 query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false);
645 query.SetValue(DICOM_TAG_SOP_INSTANCE_UID, instanceUid, false);
646 MoveInternal(targetAet, ResourceType_Instance, query);
647 }
648
649
650 void DicomControlUserConnection::FindWorklist(DicomFindAnswers& result,
651 ParsedDicomFile& query)
652 {
653 DcmDataset* dataset = query.GetDcmtkObject().getDataset();
654 const char* sopClass = UID_FINDModalityWorklistInformationModel;
655
656 FindInternal(result, dataset, sopClass, true, NULL);
657 }
658 }