comparison Core/DicomNetworking/DicomUserConnection.cpp @ 2382:7284093111b0

big reorganization to cleanly separate framework vs. server
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 29 Aug 2017 21:17:35 +0200
parents OrthancServer/DicomProtocol/DicomUserConnection.cpp@b8969010b534
children 878b59270859
comparison
equal deleted inserted replaced
2381:b8969010b534 2382:7284093111b0
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 Osimis, 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
35 /*=========================================================================
36
37 This file is based on portions of the following project:
38
39 Program: DCMTK 3.6.0
40 Module: http://dicom.offis.de/dcmtk.php.en
41
42 Copyright (C) 1994-2011, OFFIS e.V.
43 All rights reserved.
44
45 This software and supporting documentation were developed by
46
47 OFFIS e.V.
48 R&D Division Health
49 Escherweg 2
50 26121 Oldenburg, Germany
51
52 Redistribution and use in source and binary forms, with or without
53 modification, are permitted provided that the following conditions
54 are met:
55
56 - Redistributions of source code must retain the above copyright
57 notice, this list of conditions and the following disclaimer.
58
59 - Redistributions in binary form must reproduce the above copyright
60 notice, this list of conditions and the following disclaimer in the
61 documentation and/or other materials provided with the distribution.
62
63 - Neither the name of OFFIS nor the names of its contributors may be
64 used to endorse or promote products derived from this software
65 without specific prior written permission.
66
67 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
68 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
69 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
70 A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
71 HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
72 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
73 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
74 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
75 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
76 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
77 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
78
79 =========================================================================*/
80
81
82 #include "../PrecompiledHeaders.h"
83 #include "DicomUserConnection.h"
84
85 #include "../DicomFormat/DicomArray.h"
86 #include "../Logging.h"
87 #include "../OrthancException.h"
88 #include "../DicomParsing/FromDcmtkBridge.h"
89 #include "../DicomParsing/ToDcmtkBridge.h"
90
91 #include <dcmtk/dcmdata/dcistrmb.h>
92 #include <dcmtk/dcmdata/dcistrmf.h>
93 #include <dcmtk/dcmdata/dcfilefo.h>
94 #include <dcmtk/dcmdata/dcmetinf.h>
95 #include <dcmtk/dcmnet/diutil.h>
96
97 #include <set>
98
99
100 #ifdef _WIN32
101 /**
102 * "The maximum length, in bytes, of the string returned in the buffer
103 * pointed to by the name parameter is dependent on the namespace provider,
104 * but this string must be 256 bytes or less.
105 * http://msdn.microsoft.com/en-us/library/windows/desktop/ms738527(v=vs.85).aspx
106 **/
107 # define HOST_NAME_MAX 256
108 # include <winsock.h>
109 #endif
110
111
112 #if !defined(HOST_NAME_MAX) && defined(_POSIX_HOST_NAME_MAX)
113 /**
114 * TO IMPROVE: "_POSIX_HOST_NAME_MAX is only the minimum value that
115 * HOST_NAME_MAX can ever have [...] Therefore you cannot allocate an
116 * array of size _POSIX_HOST_NAME_MAX, invoke gethostname() and expect
117 * that the result will fit."
118 * http://lists.gnu.org/archive/html/bug-gnulib/2009-08/msg00128.html
119 **/
120 #define HOST_NAME_MAX _POSIX_HOST_NAME_MAX
121 #endif
122
123
124 static const char* DEFAULT_PREFERRED_TRANSFER_SYNTAX = UID_LittleEndianImplicitTransferSyntax;
125
126 /**
127 * "If we have more than 64 storage SOP classes, tools such as
128 * storescu will fail because they attempt to negotiate two
129 * presentation contexts for each SOP class, and there is a total
130 * limit of 128 contexts for one association."
131 **/
132 static const unsigned int MAXIMUM_STORAGE_SOP_CLASSES = 64;
133
134
135 namespace Orthanc
136 {
137 // By default, the timeout for DICOM SCU (client) connections is set to 10 seconds
138 static uint32_t defaultTimeout_ = 10;
139
140 struct DicomUserConnection::PImpl
141 {
142 // Connection state
143 uint32_t dimseTimeout_;
144 uint32_t acseTimeout_;
145 T_ASC_Network* net_;
146 T_ASC_Parameters* params_;
147 T_ASC_Association* assoc_;
148
149 bool IsOpen() const
150 {
151 return assoc_ != NULL;
152 }
153
154 void CheckIsOpen() const;
155
156 void Store(DcmInputStream& is,
157 DicomUserConnection& connection,
158 const std::string& moveOriginatorAET,
159 uint16_t moveOriginatorID);
160 };
161
162
163 static void Check(const OFCondition& cond)
164 {
165 if (cond.bad())
166 {
167 LOG(ERROR) << "DicomUserConnection: " << std::string(cond.text());
168 throw OrthancException(ErrorCode_NetworkProtocol);
169 }
170 }
171
172 void DicomUserConnection::PImpl::CheckIsOpen() const
173 {
174 if (!IsOpen())
175 {
176 LOG(ERROR) << "DicomUserConnection: First open the connection";
177 throw OrthancException(ErrorCode_NetworkProtocol);
178 }
179 }
180
181
182 void DicomUserConnection::CheckIsOpen() const
183 {
184 pimpl_->CheckIsOpen();
185 }
186
187
188 static void RegisterStorageSOPClass(T_ASC_Parameters* params,
189 unsigned int& presentationContextId,
190 const std::string& sopClass,
191 const char* asPreferred[],
192 std::vector<const char*>& asFallback)
193 {
194 Check(ASC_addPresentationContext(params, presentationContextId,
195 sopClass.c_str(), asPreferred, 1));
196 presentationContextId += 2;
197
198 if (asFallback.size() > 0)
199 {
200 Check(ASC_addPresentationContext(params, presentationContextId,
201 sopClass.c_str(), &asFallback[0], asFallback.size()));
202 presentationContextId += 2;
203 }
204 }
205
206
207 void DicomUserConnection::SetupPresentationContexts(const std::string& preferredTransferSyntax)
208 {
209 // Flatten an array with the preferred transfer syntax
210 const char* asPreferred[1] = { preferredTransferSyntax.c_str() };
211
212 // Setup the fallback transfer syntaxes
213 std::set<std::string> fallbackSyntaxes;
214 fallbackSyntaxes.insert(UID_LittleEndianExplicitTransferSyntax);
215 fallbackSyntaxes.insert(UID_BigEndianExplicitTransferSyntax);
216 fallbackSyntaxes.insert(UID_LittleEndianImplicitTransferSyntax);
217 fallbackSyntaxes.erase(preferredTransferSyntax);
218
219 // Flatten an array with the fallback transfer syntaxes
220 std::vector<const char*> asFallback;
221 asFallback.reserve(fallbackSyntaxes.size());
222 for (std::set<std::string>::const_iterator
223 it = fallbackSyntaxes.begin(); it != fallbackSyntaxes.end(); ++it)
224 {
225 asFallback.push_back(it->c_str());
226 }
227
228 CheckStorageSOPClassesInvariant();
229 unsigned int presentationContextId = 1;
230
231 for (std::list<std::string>::const_iterator it = reservedStorageSOPClasses_.begin();
232 it != reservedStorageSOPClasses_.end(); ++it)
233 {
234 RegisterStorageSOPClass(pimpl_->params_, presentationContextId,
235 *it, asPreferred, asFallback);
236 }
237
238 for (std::set<std::string>::const_iterator it = storageSOPClasses_.begin();
239 it != storageSOPClasses_.end(); ++it)
240 {
241 RegisterStorageSOPClass(pimpl_->params_, presentationContextId,
242 *it, asPreferred, asFallback);
243 }
244
245 for (std::set<std::string>::const_iterator it = defaultStorageSOPClasses_.begin();
246 it != defaultStorageSOPClasses_.end(); ++it)
247 {
248 RegisterStorageSOPClass(pimpl_->params_, presentationContextId,
249 *it, asPreferred, asFallback);
250 }
251 }
252
253
254 static bool IsGenericTransferSyntax(const std::string& syntax)
255 {
256 return (syntax == UID_LittleEndianExplicitTransferSyntax ||
257 syntax == UID_BigEndianExplicitTransferSyntax ||
258 syntax == UID_LittleEndianImplicitTransferSyntax);
259 }
260
261
262 void DicomUserConnection::PImpl::Store(DcmInputStream& is,
263 DicomUserConnection& connection,
264 const std::string& moveOriginatorAET,
265 uint16_t moveOriginatorID)
266 {
267 CheckIsOpen();
268
269 DcmFileFormat dcmff;
270 Check(dcmff.read(is, EXS_Unknown, EGL_noChange, DCM_MaxReadLength));
271
272 // Determine the storage SOP class UID for this instance
273 static const DcmTagKey DCM_SOP_CLASS_UID(0x0008, 0x0016);
274 OFString sopClassUid;
275 if (dcmff.getDataset()->findAndGetOFString(DCM_SOP_CLASS_UID, sopClassUid).good())
276 {
277 connection.AddStorageSOPClass(sopClassUid.c_str());
278 }
279
280 // Determine whether a new presentation context must be
281 // negotiated, depending on the transfer syntax of this instance
282 DcmXfer xfer(dcmff.getDataset()->getOriginalXfer());
283 const std::string syntax(xfer.getXferID());
284 bool isGeneric = IsGenericTransferSyntax(syntax);
285
286 bool renegotiate;
287 if (isGeneric)
288 {
289 // Are we making a generic-to-specific or specific-to-generic change of
290 // the transfer syntax? If this is the case, renegotiate the connection.
291 renegotiate = !IsGenericTransferSyntax(connection.GetPreferredTransferSyntax());
292 }
293 else
294 {
295 // We are using a specific transfer syntax. Renegotiate if the
296 // current connection does not match this transfer syntax.
297 renegotiate = (syntax != connection.GetPreferredTransferSyntax());
298 }
299
300 if (renegotiate)
301 {
302 LOG(INFO) << "Change in the transfer syntax: the C-Store associated must be renegotiated";
303
304 if (isGeneric)
305 {
306 connection.ResetPreferredTransferSyntax();
307 }
308 else
309 {
310 connection.SetPreferredTransferSyntax(syntax);
311 }
312 }
313
314 if (!connection.IsOpen())
315 {
316 LOG(INFO) << "Renegotiating a C-Store association due to a change in the parameters";
317 connection.Open();
318 }
319
320 // Figure out which SOP class and SOP instance is encapsulated in the file
321 DIC_UI sopClass;
322 DIC_UI sopInstance;
323 if (!DU_findSOPClassAndInstanceInDataSet(dcmff.getDataset(), sopClass, sopInstance))
324 {
325 throw OrthancException(ErrorCode_NoSopClassOrInstance);
326 }
327
328 // Figure out which of the accepted presentation contexts should be used
329 int presID = ASC_findAcceptedPresentationContextID(assoc_, sopClass);
330 if (presID == 0)
331 {
332 const char *modalityName = dcmSOPClassUIDToModality(sopClass);
333 if (!modalityName) modalityName = dcmFindNameOfUID(sopClass);
334 if (!modalityName) modalityName = "unknown SOP class";
335 throw OrthancException(ErrorCode_NoPresentationContext);
336 }
337
338 // Prepare the transmission of data
339 T_DIMSE_C_StoreRQ request;
340 memset(&request, 0, sizeof(request));
341 request.MessageID = assoc_->nextMsgID++;
342 strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN);
343 request.Priority = DIMSE_PRIORITY_MEDIUM;
344 request.DataSetType = DIMSE_DATASET_PRESENT;
345 strncpy(request.AffectedSOPInstanceUID, sopInstance, DIC_UI_LEN);
346
347 if (!moveOriginatorAET.empty())
348 {
349 strncpy(request.MoveOriginatorApplicationEntityTitle,
350 moveOriginatorAET.c_str(), DIC_AE_LEN);
351 request.opts = O_STORE_MOVEORIGINATORAETITLE;
352
353 request.MoveOriginatorID = moveOriginatorID; // The type DIC_US is an alias for uint16_t
354 request.opts |= O_STORE_MOVEORIGINATORID;
355 }
356
357 // Finally conduct transmission of data
358 T_DIMSE_C_StoreRSP rsp;
359 DcmDataset* statusDetail = NULL;
360 Check(DIMSE_storeUser(assoc_, presID, &request,
361 NULL, dcmff.getDataset(), /*progressCallback*/ NULL, NULL,
362 /*opt_blockMode*/ DIMSE_BLOCKING, /*opt_dimse_timeout*/ dimseTimeout_,
363 &rsp, &statusDetail, NULL));
364
365 if (statusDetail != NULL)
366 {
367 delete statusDetail;
368 }
369 }
370
371
372 namespace
373 {
374 struct FindPayload
375 {
376 DicomFindAnswers* answers;
377 const char* level;
378 bool isWorklist;
379 };
380 }
381
382
383 static void FindCallback(
384 /* in */
385 void *callbackData,
386 T_DIMSE_C_FindRQ *request, /* original find request */
387 int responseCount,
388 T_DIMSE_C_FindRSP *response, /* pending response received */
389 DcmDataset *responseIdentifiers /* pending response identifiers */
390 )
391 {
392 FindPayload& payload = *reinterpret_cast<FindPayload*>(callbackData);
393
394 if (responseIdentifiers != NULL)
395 {
396 if (payload.isWorklist)
397 {
398 ParsedDicomFile answer(*responseIdentifiers);
399 payload.answers->Add(answer);
400 }
401 else
402 {
403 DicomMap m;
404 FromDcmtkBridge::ExtractDicomSummary(m, *responseIdentifiers);
405
406 if (!m.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL))
407 {
408 m.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, payload.level, false);
409 }
410
411 payload.answers->Add(m);
412 }
413 }
414 }
415
416
417 static void FixFindQuery(DicomMap& fixedQuery,
418 ResourceType level,
419 const DicomMap& fields)
420 {
421 std::set<DicomTag> allowedTags;
422
423 // WARNING: Do not add "break" or reorder items in this switch-case!
424 switch (level)
425 {
426 case ResourceType_Instance:
427 DicomTag::AddTagsForModule(allowedTags, DicomModule_Instance);
428
429 case ResourceType_Series:
430 DicomTag::AddTagsForModule(allowedTags, DicomModule_Series);
431
432 case ResourceType_Study:
433 DicomTag::AddTagsForModule(allowedTags, DicomModule_Study);
434
435 case ResourceType_Patient:
436 DicomTag::AddTagsForModule(allowedTags, DicomModule_Patient);
437 break;
438
439 default:
440 throw OrthancException(ErrorCode_InternalError);
441 }
442
443 switch (level)
444 {
445 case ResourceType_Patient:
446 allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES);
447 allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES);
448 allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES);
449 break;
450
451 case ResourceType_Study:
452 allowedTags.insert(DICOM_TAG_MODALITIES_IN_STUDY);
453 allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES);
454 allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES);
455 allowedTags.insert(DICOM_TAG_SOP_CLASSES_IN_STUDY);
456 break;
457
458 case ResourceType_Series:
459 allowedTags.insert(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES);
460 break;
461
462 default:
463 break;
464 }
465
466 allowedTags.insert(DICOM_TAG_SPECIFIC_CHARACTER_SET);
467
468 DicomArray query(fields);
469 for (size_t i = 0; i < query.GetSize(); i++)
470 {
471 const DicomTag& tag = query.GetElement(i).GetTag();
472 if (allowedTags.find(tag) == allowedTags.end())
473 {
474 LOG(WARNING) << "Tag not allowed for this C-Find level, will be ignored: " << tag;
475 }
476 else
477 {
478 fixedQuery.SetValue(tag, query.GetElement(i).GetValue());
479 }
480 }
481 }
482
483
484 static ParsedDicomFile* ConvertQueryFields(const DicomMap& fields,
485 ModalityManufacturer manufacturer)
486 {
487 // Fix outgoing C-Find requests issue for Syngo.Via and its
488 // solution was reported by Emsy Chan by private mail on
489 // 2015-06-17. According to Robert van Ommen (2015-11-30), the
490 // same fix is required for Agfa Impax. This was generalized for
491 // generic manufacturer since it seems to affect PhilipsADW,
492 // GEWAServer as well:
493 // https://bitbucket.org/sjodogne/orthanc/issues/31/
494
495 switch (manufacturer)
496 {
497 case ModalityManufacturer_GenericNoWildcardInDates:
498 case ModalityManufacturer_GenericNoUniversalWildcard:
499 {
500 std::auto_ptr<DicomMap> fix(fields.Clone());
501
502 std::set<DicomTag> tags;
503 fix->GetTags(tags);
504
505 for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
506 {
507 // Replace a "*" wildcard query by an empty query ("") for
508 // "date" or "all" value representations depending on the
509 // type of manufacturer.
510 if (manufacturer == ModalityManufacturer_GenericNoUniversalWildcard ||
511 (manufacturer == ModalityManufacturer_GenericNoWildcardInDates &&
512 FromDcmtkBridge::LookupValueRepresentation(*it) == ValueRepresentation_Date))
513 {
514 const DicomValue* value = fix->TestAndGetValue(*it);
515
516 if (value != NULL &&
517 !value->IsNull() &&
518 value->GetContent() == "*")
519 {
520 fix->SetValue(*it, "", false);
521 }
522 }
523 }
524
525 return new ParsedDicomFile(*fix);
526 }
527
528 default:
529 return new ParsedDicomFile(fields);
530 }
531 }
532
533
534 static void ExecuteFind(DicomFindAnswers& answers,
535 T_ASC_Association* association,
536 DcmDataset* dataset,
537 const char* sopClass,
538 bool isWorklist,
539 const char* level,
540 uint32_t dimseTimeout)
541 {
542 assert(isWorklist ^ (level != NULL));
543
544 FindPayload payload;
545 payload.answers = &answers;
546 payload.level = level;
547 payload.isWorklist = isWorklist;
548
549 // Figure out which of the accepted presentation contexts should be used
550 int presID = ASC_findAcceptedPresentationContextID(association, sopClass);
551 if (presID == 0)
552 {
553 throw OrthancException(ErrorCode_DicomFindUnavailable);
554 }
555
556 T_DIMSE_C_FindRQ request;
557 memset(&request, 0, sizeof(request));
558 request.MessageID = association->nextMsgID++;
559 strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN);
560 request.Priority = DIMSE_PRIORITY_MEDIUM;
561 request.DataSetType = DIMSE_DATASET_PRESENT;
562
563 T_DIMSE_C_FindRSP response;
564 DcmDataset* statusDetail = NULL;
565 OFCondition cond = DIMSE_findUser(association, presID, &request, dataset,
566 FindCallback, &payload,
567 /*opt_blockMode*/ DIMSE_BLOCKING,
568 /*opt_dimse_timeout*/ dimseTimeout,
569 &response, &statusDetail);
570
571 if (statusDetail)
572 {
573 delete statusDetail;
574 }
575
576 Check(cond);
577 }
578
579
580 void DicomUserConnection::Find(DicomFindAnswers& result,
581 ResourceType level,
582 const DicomMap& originalFields)
583 {
584 DicomMap fields;
585 FixFindQuery(fields, level, originalFields);
586
587 CheckIsOpen();
588
589 std::auto_ptr<ParsedDicomFile> query(ConvertQueryFields(fields, manufacturer_));
590 DcmDataset* dataset = query->GetDcmtkObject().getDataset();
591
592 const char* clevel = NULL;
593 const char* sopClass = NULL;
594
595 switch (level)
596 {
597 case ResourceType_Patient:
598 clevel = "PATIENT";
599 DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "PATIENT");
600 sopClass = UID_FINDPatientRootQueryRetrieveInformationModel;
601 break;
602
603 case ResourceType_Study:
604 clevel = "STUDY";
605 DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "STUDY");
606 sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
607 break;
608
609 case ResourceType_Series:
610 clevel = "SERIES";
611 DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "SERIES");
612 sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
613 break;
614
615 case ResourceType_Instance:
616 clevel = "INSTANCE";
617 if (manufacturer_ == ModalityManufacturer_ClearCanvas ||
618 manufacturer_ == ModalityManufacturer_Dcm4Chee)
619 {
620 // This is a particular case for ClearCanvas, thanks to Peter Somlo <peter.somlo@gmail.com>.
621 // https://groups.google.com/d/msg/orthanc-users/j-6C3MAVwiw/iolB9hclom8J
622 // http://www.clearcanvas.ca/Home/Community/OldForums/tabid/526/aff/11/aft/14670/afv/topic/Default.aspx
623 DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "IMAGE");
624 }
625 else
626 {
627 DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "INSTANCE");
628 }
629
630 sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
631 break;
632
633 default:
634 throw OrthancException(ErrorCode_ParameterOutOfRange);
635 }
636
637 // Add the expected tags for this query level.
638 // WARNING: Do not reorder or add "break" in this switch-case!
639 switch (level)
640 {
641 case ResourceType_Instance:
642 // SOP Instance UID
643 if (!fields.HasTag(0x0008, 0x0018))
644 DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0018), "");
645
646 case ResourceType_Series:
647 // Series instance UID
648 if (!fields.HasTag(0x0020, 0x000e))
649 DU_putStringDOElement(dataset, DcmTagKey(0x0020, 0x000e), "");
650
651 case ResourceType_Study:
652 // Accession number
653 if (!fields.HasTag(0x0008, 0x0050))
654 DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0050), "");
655
656 // Study instance UID
657 if (!fields.HasTag(0x0020, 0x000d))
658 DU_putStringDOElement(dataset, DcmTagKey(0x0020, 0x000d), "");
659
660 case ResourceType_Patient:
661 // Patient ID
662 if (!fields.HasTag(0x0010, 0x0020))
663 DU_putStringDOElement(dataset, DcmTagKey(0x0010, 0x0020), "");
664
665 break;
666
667 default:
668 throw OrthancException(ErrorCode_ParameterOutOfRange);
669 }
670
671 assert(clevel != NULL && sopClass != NULL);
672 ExecuteFind(result, pimpl_->assoc_, dataset, sopClass, false, clevel, pimpl_->dimseTimeout_);
673 }
674
675
676 void DicomUserConnection::MoveInternal(const std::string& targetAet,
677 ResourceType level,
678 const DicomMap& fields)
679 {
680 CheckIsOpen();
681
682 std::auto_ptr<ParsedDicomFile> query(ConvertQueryFields(fields, manufacturer_));
683 DcmDataset* dataset = query->GetDcmtkObject().getDataset();
684
685 const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel;
686 switch (level)
687 {
688 case ResourceType_Patient:
689 DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "PATIENT");
690 break;
691
692 case ResourceType_Study:
693 DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "STUDY");
694 break;
695
696 case ResourceType_Series:
697 DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "SERIES");
698 break;
699
700 case ResourceType_Instance:
701 if (manufacturer_ == ModalityManufacturer_ClearCanvas ||
702 manufacturer_ == ModalityManufacturer_Dcm4Chee)
703 {
704 // This is a particular case for ClearCanvas, thanks to Peter Somlo <peter.somlo@gmail.com>.
705 // https://groups.google.com/d/msg/orthanc-users/j-6C3MAVwiw/iolB9hclom8J
706 // http://www.clearcanvas.ca/Home/Community/OldForums/tabid/526/aff/11/aft/14670/afv/topic/Default.aspx
707 DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "IMAGE");
708 }
709 else
710 {
711 DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "INSTANCE");
712 }
713 break;
714
715 default:
716 throw OrthancException(ErrorCode_ParameterOutOfRange);
717 }
718
719 // Figure out which of the accepted presentation contexts should be used
720 int presID = ASC_findAcceptedPresentationContextID(pimpl_->assoc_, sopClass);
721 if (presID == 0)
722 {
723 throw OrthancException(ErrorCode_DicomMoveUnavailable);
724 }
725
726 T_DIMSE_C_MoveRQ request;
727 memset(&request, 0, sizeof(request));
728 request.MessageID = pimpl_->assoc_->nextMsgID++;
729 strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN);
730 request.Priority = DIMSE_PRIORITY_MEDIUM;
731 request.DataSetType = DIMSE_DATASET_PRESENT;
732 strncpy(request.MoveDestination, targetAet.c_str(), DIC_AE_LEN);
733
734 T_DIMSE_C_MoveRSP response;
735 DcmDataset* statusDetail = NULL;
736 DcmDataset* responseIdentifiers = NULL;
737 OFCondition cond = DIMSE_moveUser(pimpl_->assoc_, presID, &request, dataset,
738 NULL, NULL,
739 /*opt_blockMode*/ DIMSE_BLOCKING,
740 /*opt_dimse_timeout*/ pimpl_->dimseTimeout_,
741 pimpl_->net_, NULL, NULL,
742 &response, &statusDetail, &responseIdentifiers);
743
744 if (statusDetail)
745 {
746 delete statusDetail;
747 }
748
749 if (responseIdentifiers)
750 {
751 delete responseIdentifiers;
752 }
753
754 Check(cond);
755 }
756
757
758 void DicomUserConnection::ResetStorageSOPClasses()
759 {
760 CheckStorageSOPClassesInvariant();
761
762 storageSOPClasses_.clear();
763 defaultStorageSOPClasses_.clear();
764
765 // Copy the short list of storage SOP classes from DCMTK, making
766 // room for the 5 SOP classes reserved for C-ECHO, C-FIND, C-MOVE at (**).
767
768 std::set<std::string> uncommon;
769 uncommon.insert(UID_BlendingSoftcopyPresentationStateStorage);
770 uncommon.insert(UID_GrayscaleSoftcopyPresentationStateStorage);
771 uncommon.insert(UID_ColorSoftcopyPresentationStateStorage);
772 uncommon.insert(UID_PseudoColorSoftcopyPresentationStateStorage);
773 uncommon.insert(UID_XAXRFGrayscaleSoftcopyPresentationStateStorage);
774
775 // Add the storage syntaxes for C-STORE
776 for (int i = 0; i < numberOfDcmShortSCUStorageSOPClassUIDs - 1; i++)
777 {
778 if (uncommon.find(dcmShortSCUStorageSOPClassUIDs[i]) == uncommon.end())
779 {
780 defaultStorageSOPClasses_.insert(dcmShortSCUStorageSOPClassUIDs[i]);
781 }
782 }
783
784 CheckStorageSOPClassesInvariant();
785 }
786
787
788 DicomUserConnection::DicomUserConnection() :
789 pimpl_(new PImpl),
790 preferredTransferSyntax_(DEFAULT_PREFERRED_TRANSFER_SYNTAX),
791 localAet_("STORESCU"),
792 remoteAet_("ANY-SCP"),
793 remoteHost_("127.0.0.1")
794 {
795 remotePort_ = 104;
796 manufacturer_ = ModalityManufacturer_Generic;
797
798 SetTimeout(defaultTimeout_);
799 pimpl_->net_ = NULL;
800 pimpl_->params_ = NULL;
801 pimpl_->assoc_ = NULL;
802
803 // SOP classes for C-ECHO, C-FIND and C-MOVE (**)
804 reservedStorageSOPClasses_.push_back(UID_VerificationSOPClass);
805 reservedStorageSOPClasses_.push_back(UID_FINDPatientRootQueryRetrieveInformationModel);
806 reservedStorageSOPClasses_.push_back(UID_FINDStudyRootQueryRetrieveInformationModel);
807 reservedStorageSOPClasses_.push_back(UID_MOVEStudyRootQueryRetrieveInformationModel);
808 reservedStorageSOPClasses_.push_back(UID_FINDModalityWorklistInformationModel);
809
810 ResetStorageSOPClasses();
811 }
812
813 DicomUserConnection::~DicomUserConnection()
814 {
815 Close();
816 }
817
818
819 void DicomUserConnection::SetRemoteModality(const RemoteModalityParameters& parameters)
820 {
821 SetRemoteApplicationEntityTitle(parameters.GetApplicationEntityTitle());
822 SetRemoteHost(parameters.GetHost());
823 SetRemotePort(parameters.GetPort());
824 SetRemoteManufacturer(parameters.GetManufacturer());
825 }
826
827
828 void DicomUserConnection::SetLocalApplicationEntityTitle(const std::string& aet)
829 {
830 if (localAet_ != aet)
831 {
832 Close();
833 localAet_ = aet;
834 }
835 }
836
837 void DicomUserConnection::SetRemoteApplicationEntityTitle(const std::string& aet)
838 {
839 if (remoteAet_ != aet)
840 {
841 Close();
842 remoteAet_ = aet;
843 }
844 }
845
846 void DicomUserConnection::SetRemoteManufacturer(ModalityManufacturer manufacturer)
847 {
848 if (manufacturer_ != manufacturer)
849 {
850 Close();
851 manufacturer_ = manufacturer;
852 }
853 }
854
855 void DicomUserConnection::ResetPreferredTransferSyntax()
856 {
857 SetPreferredTransferSyntax(DEFAULT_PREFERRED_TRANSFER_SYNTAX);
858 }
859
860 void DicomUserConnection::SetPreferredTransferSyntax(const std::string& preferredTransferSyntax)
861 {
862 if (preferredTransferSyntax_ != preferredTransferSyntax)
863 {
864 Close();
865 preferredTransferSyntax_ = preferredTransferSyntax;
866 }
867 }
868
869
870 void DicomUserConnection::SetRemoteHost(const std::string& host)
871 {
872 if (remoteHost_ != host)
873 {
874 if (host.size() > HOST_NAME_MAX - 10)
875 {
876 throw OrthancException(ErrorCode_ParameterOutOfRange);
877 }
878
879 Close();
880 remoteHost_ = host;
881 }
882 }
883
884 void DicomUserConnection::SetRemotePort(uint16_t port)
885 {
886 if (remotePort_ != port)
887 {
888 Close();
889 remotePort_ = port;
890 }
891 }
892
893 void DicomUserConnection::Open()
894 {
895 if (IsOpen())
896 {
897 // Don't reopen the connection
898 return;
899 }
900
901 LOG(INFO) << "Opening a DICOM SCU connection from AET \"" << GetLocalApplicationEntityTitle()
902 << "\" to AET \"" << GetRemoteApplicationEntityTitle() << "\" on host "
903 << GetRemoteHost() << ":" << GetRemotePort()
904 << " (manufacturer: " << EnumerationToString(GetRemoteManufacturer()) << ")";
905
906 Check(ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ pimpl_->acseTimeout_, &pimpl_->net_));
907 Check(ASC_createAssociationParameters(&pimpl_->params_, /*opt_maxReceivePDULength*/ ASC_DEFAULTMAXPDU));
908
909 // Set this application's title and the called application's title in the params
910 Check(ASC_setAPTitles(pimpl_->params_, localAet_.c_str(), remoteAet_.c_str(), NULL));
911
912 // Set the network addresses of the local and remote entities
913 char localHost[HOST_NAME_MAX];
914 gethostname(localHost, HOST_NAME_MAX - 1);
915
916 char remoteHostAndPort[HOST_NAME_MAX];
917
918 #ifdef _MSC_VER
919 _snprintf
920 #else
921 snprintf
922 #endif
923 (remoteHostAndPort, HOST_NAME_MAX - 1, "%s:%d", remoteHost_.c_str(), remotePort_);
924
925 Check(ASC_setPresentationAddresses(pimpl_->params_, localHost, remoteHostAndPort));
926
927 // Set various options
928 Check(ASC_setTransportLayerType(pimpl_->params_, /*opt_secureConnection*/ false));
929
930 SetupPresentationContexts(preferredTransferSyntax_);
931
932 // Do the association
933 Check(ASC_requestAssociation(pimpl_->net_, pimpl_->params_, &pimpl_->assoc_));
934
935 if (ASC_countAcceptedPresentationContexts(pimpl_->params_) == 0)
936 {
937 throw OrthancException(ErrorCode_NoPresentationContext);
938 }
939 }
940
941 void DicomUserConnection::Close()
942 {
943 if (pimpl_->assoc_ != NULL)
944 {
945 ASC_releaseAssociation(pimpl_->assoc_);
946 ASC_destroyAssociation(&pimpl_->assoc_);
947 pimpl_->assoc_ = NULL;
948 pimpl_->params_ = NULL;
949 }
950 else
951 {
952 if (pimpl_->params_ != NULL)
953 {
954 ASC_destroyAssociationParameters(&pimpl_->params_);
955 pimpl_->params_ = NULL;
956 }
957 }
958
959 if (pimpl_->net_ != NULL)
960 {
961 ASC_dropNetwork(&pimpl_->net_);
962 pimpl_->net_ = NULL;
963 }
964 }
965
966 bool DicomUserConnection::IsOpen() const
967 {
968 return pimpl_->IsOpen();
969 }
970
971 void DicomUserConnection::Store(const char* buffer,
972 size_t size,
973 const std::string& moveOriginatorAET,
974 uint16_t moveOriginatorID)
975 {
976 // Prepare an input stream for the memory buffer
977 DcmInputBufferStream is;
978 if (size > 0)
979 is.setBuffer(buffer, size);
980 is.setEos();
981
982 pimpl_->Store(is, *this, moveOriginatorAET, moveOriginatorID);
983 }
984
985 void DicomUserConnection::Store(const std::string& buffer,
986 const std::string& moveOriginatorAET,
987 uint16_t moveOriginatorID)
988 {
989 if (buffer.size() > 0)
990 Store(reinterpret_cast<const char*>(&buffer[0]), buffer.size(), moveOriginatorAET, moveOriginatorID);
991 else
992 Store(NULL, 0, moveOriginatorAET, moveOriginatorID);
993 }
994
995 void DicomUserConnection::StoreFile(const std::string& path,
996 const std::string& moveOriginatorAET,
997 uint16_t moveOriginatorID)
998 {
999 // Prepare an input stream for the file
1000 DcmInputFileStream is(path.c_str());
1001 pimpl_->Store(is, *this, moveOriginatorAET, moveOriginatorID);
1002 }
1003
1004 bool DicomUserConnection::Echo()
1005 {
1006 CheckIsOpen();
1007 DIC_US status;
1008 Check(DIMSE_echoUser(pimpl_->assoc_, pimpl_->assoc_->nextMsgID++,
1009 /*opt_blockMode*/ DIMSE_BLOCKING,
1010 /*opt_dimse_timeout*/ pimpl_->dimseTimeout_,
1011 &status, NULL));
1012 return status == STATUS_Success;
1013 }
1014
1015
1016 static void TestAndCopyTag(DicomMap& result,
1017 const DicomMap& source,
1018 const DicomTag& tag)
1019 {
1020 if (!source.HasTag(tag))
1021 {
1022 throw OrthancException(ErrorCode_BadRequest);
1023 }
1024 else
1025 {
1026 result.SetValue(tag, source.GetValue(tag));
1027 }
1028 }
1029
1030
1031 void DicomUserConnection::Move(const std::string& targetAet,
1032 ResourceType level,
1033 const DicomMap& findResult)
1034 {
1035 DicomMap move;
1036 switch (level)
1037 {
1038 case ResourceType_Patient:
1039 TestAndCopyTag(move, findResult, DICOM_TAG_PATIENT_ID);
1040 break;
1041
1042 case ResourceType_Study:
1043 TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
1044 break;
1045
1046 case ResourceType_Series:
1047 TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
1048 TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID);
1049 break;
1050
1051 case ResourceType_Instance:
1052 TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
1053 TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID);
1054 TestAndCopyTag(move, findResult, DICOM_TAG_SOP_INSTANCE_UID);
1055 break;
1056
1057 default:
1058 throw OrthancException(ErrorCode_InternalError);
1059 }
1060
1061 MoveInternal(targetAet, level, move);
1062 }
1063
1064
1065 void DicomUserConnection::Move(const std::string& targetAet,
1066 const DicomMap& findResult)
1067 {
1068 if (!findResult.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL))
1069 {
1070 throw OrthancException(ErrorCode_InternalError);
1071 }
1072
1073 const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent();
1074 ResourceType level = StringToResourceType(tmp.c_str());
1075
1076 Move(targetAet, level, findResult);
1077 }
1078
1079
1080 void DicomUserConnection::MovePatient(const std::string& targetAet,
1081 const std::string& patientId)
1082 {
1083 DicomMap query;
1084 query.SetValue(DICOM_TAG_PATIENT_ID, patientId, false);
1085 MoveInternal(targetAet, ResourceType_Patient, query);
1086 }
1087
1088 void DicomUserConnection::MoveStudy(const std::string& targetAet,
1089 const std::string& studyUid)
1090 {
1091 DicomMap query;
1092 query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false);
1093 MoveInternal(targetAet, ResourceType_Study, query);
1094 }
1095
1096 void DicomUserConnection::MoveSeries(const std::string& targetAet,
1097 const std::string& studyUid,
1098 const std::string& seriesUid)
1099 {
1100 DicomMap query;
1101 query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false);
1102 query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false);
1103 MoveInternal(targetAet, ResourceType_Series, query);
1104 }
1105
1106 void DicomUserConnection::MoveInstance(const std::string& targetAet,
1107 const std::string& studyUid,
1108 const std::string& seriesUid,
1109 const std::string& instanceUid)
1110 {
1111 DicomMap query;
1112 query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false);
1113 query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false);
1114 query.SetValue(DICOM_TAG_SOP_INSTANCE_UID, instanceUid, false);
1115 MoveInternal(targetAet, ResourceType_Instance, query);
1116 }
1117
1118
1119 void DicomUserConnection::SetTimeout(uint32_t seconds)
1120 {
1121 if (seconds == 0)
1122 {
1123 DisableTimeout();
1124 }
1125 else
1126 {
1127 dcmConnectionTimeout.set(seconds);
1128 pimpl_->dimseTimeout_ = seconds;
1129 pimpl_->acseTimeout_ = 10; // Timeout used during association negociation
1130 }
1131 }
1132
1133
1134 void DicomUserConnection::DisableTimeout()
1135 {
1136 /**
1137 * Global timeout (seconds) for connecting to remote hosts.
1138 * Default value is -1 which selects infinite timeout, i.e. blocking connect().
1139 */
1140 dcmConnectionTimeout.set(-1);
1141 pimpl_->dimseTimeout_ = 0;
1142 pimpl_->acseTimeout_ = 10; // Timeout used during association negociation
1143 }
1144
1145
1146 void DicomUserConnection::CheckStorageSOPClassesInvariant() const
1147 {
1148 assert(storageSOPClasses_.size() +
1149 defaultStorageSOPClasses_.size() +
1150 reservedStorageSOPClasses_.size() <= MAXIMUM_STORAGE_SOP_CLASSES);
1151 }
1152
1153 void DicomUserConnection::AddStorageSOPClass(const char* sop)
1154 {
1155 CheckStorageSOPClassesInvariant();
1156
1157 if (storageSOPClasses_.find(sop) != storageSOPClasses_.end())
1158 {
1159 // This storage SOP class is already explicitly registered. Do
1160 // nothing.
1161 return;
1162 }
1163
1164 if (defaultStorageSOPClasses_.find(sop) != defaultStorageSOPClasses_.end())
1165 {
1166 // This storage SOP class is not explicitly registered, but is
1167 // used by default. Just register it explicitly.
1168 defaultStorageSOPClasses_.erase(sop);
1169 storageSOPClasses_.insert(sop);
1170
1171 CheckStorageSOPClassesInvariant();
1172 return;
1173 }
1174
1175 // This storage SOP class is neither explicitly, nor implicitly
1176 // registered. Close the connection and register it explicitly.
1177
1178 Close();
1179
1180 if (reservedStorageSOPClasses_.size() +
1181 storageSOPClasses_.size() >= MAXIMUM_STORAGE_SOP_CLASSES) // (*)
1182 {
1183 // The maximum number of SOP classes is reached
1184 ResetStorageSOPClasses();
1185 defaultStorageSOPClasses_.erase(sop);
1186 }
1187 else if (reservedStorageSOPClasses_.size() + storageSOPClasses_.size() +
1188 defaultStorageSOPClasses_.size() >= MAXIMUM_STORAGE_SOP_CLASSES)
1189 {
1190 // Make room in the default storage syntaxes
1191 assert(!defaultStorageSOPClasses_.empty()); // Necessarily true because condition (*) is false
1192 defaultStorageSOPClasses_.erase(*defaultStorageSOPClasses_.rbegin());
1193 }
1194
1195 // Explicitly register the new storage syntax
1196 storageSOPClasses_.insert(sop);
1197
1198 CheckStorageSOPClassesInvariant();
1199 }
1200
1201
1202 void DicomUserConnection::FindWorklist(DicomFindAnswers& result,
1203 ParsedDicomFile& query)
1204 {
1205 CheckIsOpen();
1206
1207 DcmDataset* dataset = query.GetDcmtkObject().getDataset();
1208 const char* sopClass = UID_FINDModalityWorklistInformationModel;
1209
1210 ExecuteFind(result, pimpl_->assoc_, dataset, sopClass, true, NULL, pimpl_->dimseTimeout_);
1211 }
1212
1213
1214 void DicomUserConnection::SetDefaultTimeout(uint32_t seconds)
1215 {
1216 LOG(INFO) << "Default timeout for DICOM connections if Orthanc acts as SCU (client): "
1217 << seconds << " seconds (0 = no timeout)";
1218 defaultTimeout_ = seconds;
1219 }
1220 }