comparison OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp @ 4044:d25f4c0fa160 framework

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