comparison Core/DicomNetworking/DicomControlUserConnection.cpp @ 3827:638906dcfe32 transcoding

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