Mercurial > hg > orthanc
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 } |