comparison OrthancServer/Sources/OrthancGetRequestHandler.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 OrthancServer/OrthancGetRequestHandler.cpp@7f8b30416d50
children 05b8fd21089c
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 #include "PrecompiledHeadersServer.h"
34 #include "OrthancGetRequestHandler.h"
35
36 #include "../../Core/DicomParsing/FromDcmtkBridge.h"
37 #include "../Core/DicomFormat/DicomArray.h"
38 #include "../Core/Logging.h"
39 #include "../Core/MetricsRegistry.h"
40 #include "OrthancConfiguration.h"
41 #include "ServerContext.h"
42 #include "ServerJobs/DicomModalityStoreJob.h"
43
44 #include <dcmtk/dcmdata/dcdeftag.h>
45 #include <dcmtk/dcmdata/dcfilefo.h>
46 #include <dcmtk/dcmdata/dcistrmb.h>
47 #include <dcmtk/dcmnet/assoc.h>
48 #include <dcmtk/dcmnet/dimse.h>
49 #include <dcmtk/dcmnet/diutil.h>
50 #include <dcmtk/ofstd/ofstring.h>
51
52 #include <sstream> // For std::stringstream
53
54 namespace Orthanc
55 {
56 namespace
57 {
58 // Anonymous namespace to avoid clashes between compilation modules
59
60 static void GetSubOpProgressCallback(
61 void * /* callbackData == pointer to the "OrthancGetRequestHandler" object */,
62 T_DIMSE_StoreProgress *progress,
63 T_DIMSE_C_StoreRQ * /*req*/)
64 {
65 // SBL - no logging to be done here.
66 }
67 }
68
69 OrthancGetRequestHandler::Status
70 OrthancGetRequestHandler::DoNext(T_ASC_Association* assoc)
71 {
72 if (position_ >= instances_.size())
73 {
74 return Status_Failure;
75 }
76
77 const std::string& id = instances_[position_++];
78
79 std::string dicom;
80 context_.ReadDicom(dicom, id);
81
82 if (dicom.size() <= 0)
83 {
84 return Status_Failure;
85 }
86
87 std::unique_ptr<DcmFileFormat> parsed(
88 FromDcmtkBridge::LoadFromMemoryBuffer(dicom.c_str(), dicom.size()));
89
90 if (parsed.get() == NULL ||
91 parsed->getDataset() == NULL)
92 {
93 throw OrthancException(ErrorCode_InternalError);
94 }
95
96 DcmDataset& dataset = *parsed->getDataset();
97
98 OFString a, b;
99 if (!dataset.findAndGetOFString(DCM_SOPClassUID, a).good() ||
100 !dataset.findAndGetOFString(DCM_SOPInstanceUID, b).good())
101 {
102 throw OrthancException(ErrorCode_NoSopClassOrInstance,
103 "Unable to determine the SOP class/instance for C-STORE with AET " +
104 originatorAet_);
105 }
106
107 std::string sopClassUid(a.c_str());
108 std::string sopInstanceUid(b.c_str());
109
110 OFCondition cond = PerformGetSubOp(assoc, sopClassUid, sopInstanceUid, parsed.release());
111
112 if (getCancelled_)
113 {
114 LOG(INFO) << "C-GET SCP: Received C-Cancel RQ";
115 }
116
117 if (cond.bad() || getCancelled_)
118 {
119 return Status_Failure;
120 }
121
122 return Status_Success;
123 }
124
125
126 void OrthancGetRequestHandler::AddFailedUIDInstance(const std::string& sopInstance)
127 {
128 if (failedUIDs_.empty())
129 {
130 failedUIDs_ = sopInstance;
131 }
132 else
133 {
134 failedUIDs_ += "\\" + sopInstance;
135 }
136 }
137
138
139 static bool SelectPresentationContext(T_ASC_PresentationContextID& selectedPresentationId,
140 DicomTransferSyntax& selectedSyntax,
141 T_ASC_Association* assoc,
142 const std::string& sopClassUid,
143 DicomTransferSyntax sourceSyntax,
144 bool allowTranscoding)
145 {
146 typedef std::map<DicomTransferSyntax, T_ASC_PresentationContextID> Accepted;
147
148 Accepted accepted;
149
150 /**
151 * 1. Inspect and index all the accepted transfer syntaxes. This
152 * is similar to the code from "DicomAssociation::Open()".
153 **/
154
155 LST_HEAD **l = &assoc->params->DULparams.acceptedPresentationContext;
156 if (*l != NULL)
157 {
158 DUL_PRESENTATIONCONTEXT* pc = (DUL_PRESENTATIONCONTEXT*) LST_Head(l);
159 LST_Position(l, (LST_NODE*)pc);
160 while (pc)
161 {
162 DicomTransferSyntax transferSyntax;
163 if (pc->result == ASC_P_ACCEPTANCE &&
164 LookupTransferSyntax(transferSyntax, pc->acceptedTransferSyntax))
165 {
166 VLOG(0) << "C-GET SCP accepted: SOP class " << sopClassUid
167 << " with transfer syntax " << GetTransferSyntaxUid(transferSyntax);
168 if (std::string(pc->abstractSyntax) == sopClassUid)
169 {
170 accepted[transferSyntax] = pc->presentationContextID;
171 }
172 }
173 else
174 {
175 LOG(WARNING) << "C-GET: Unknown transfer syntax received: "
176 << pc->acceptedTransferSyntax;
177 }
178
179 pc = (DUL_PRESENTATIONCONTEXT*) LST_Next(l);
180 }
181 }
182
183
184 /**
185 * 2. Select the preferred transfer syntaxes, which corresponds to
186 * the source transfer syntax, plus all the uncompressed transfer
187 * syntaxes if transcoding is enabled.
188 **/
189
190 std::list<DicomTransferSyntax> preferred;
191 preferred.push_back(sourceSyntax);
192
193 if (allowTranscoding)
194 {
195 if (sourceSyntax != DicomTransferSyntax_LittleEndianImplicit)
196 {
197 // Default Transfer Syntax for DICOM
198 preferred.push_back(DicomTransferSyntax_LittleEndianImplicit);
199 }
200
201 if (sourceSyntax != DicomTransferSyntax_LittleEndianExplicit)
202 {
203 preferred.push_back(DicomTransferSyntax_LittleEndianExplicit);
204 }
205
206 if (sourceSyntax != DicomTransferSyntax_BigEndianExplicit)
207 {
208 // Retired
209 preferred.push_back(DicomTransferSyntax_BigEndianExplicit);
210 }
211 }
212
213
214 /**
215 * 3. Lookup whether one of the preferred transfer syntaxes was
216 * accepted.
217 **/
218
219 for (std::list<DicomTransferSyntax>::const_iterator
220 it = preferred.begin(); it != preferred.end(); ++it)
221 {
222 Accepted::const_iterator found = accepted.find(*it);
223 if (found != accepted.end())
224 {
225 selectedPresentationId = found->second;
226 selectedSyntax = *it;
227 return true;
228 }
229 }
230
231 // No preferred syntax was accepted
232 return false;
233 }
234
235
236 OFCondition OrthancGetRequestHandler::PerformGetSubOp(T_ASC_Association* assoc,
237 const std::string& sopClassUid,
238 const std::string& sopInstanceUid,
239 DcmFileFormat* dicomRaw)
240 {
241 assert(dicomRaw != NULL);
242 std::unique_ptr<DcmFileFormat> dicom(dicomRaw);
243
244 DicomTransferSyntax sourceSyntax;
245 if (!FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, *dicom))
246 {
247 nFailed_++;
248 AddFailedUIDInstance(sopInstanceUid);
249 LOG(ERROR) << "C-GET SCP: Unknown transfer syntax: ("
250 << dcmSOPClassUIDToModality(sopClassUid.c_str(), "OT") << ") " << sopClassUid;
251 return DIMSE_NOVALIDPRESENTATIONCONTEXTID;
252 }
253
254 bool allowTranscoding = (context_.IsTranscodeDicomProtocol() &&
255 remote_.IsTranscodingAllowed());
256
257 T_ASC_PresentationContextID presId = 0; // Unnecessary initialization, makes code clearer
258 DicomTransferSyntax selectedSyntax;
259 if (!SelectPresentationContext(presId, selectedSyntax, assoc, sopClassUid,
260 sourceSyntax, allowTranscoding) ||
261 presId == 0)
262 {
263 nFailed_++;
264 AddFailedUIDInstance(sopInstanceUid);
265 LOG(ERROR) << "C-GET SCP: storeSCU: No presentation context for: ("
266 << dcmSOPClassUIDToModality(sopClassUid.c_str(), "OT") << ") " << sopClassUid;
267 return DIMSE_NOVALIDPRESENTATIONCONTEXTID;
268 }
269 else
270 {
271 LOG(INFO) << "C-GET SCP selected transfer syntax " << GetTransferSyntaxUid(selectedSyntax)
272 << ", for source instance with SOP class " << sopClassUid
273 << " and transfer syntax " << GetTransferSyntaxUid(sourceSyntax);
274
275 // make sure that we can send images in this presentation context
276 T_ASC_PresentationContext pc;
277 ASC_findAcceptedPresentationContext(assoc->params, presId, &pc);
278 // the acceptedRole is the association requestor role
279
280 if (pc.acceptedRole != ASC_SC_ROLE_DEFAULT && // "DEFAULT" is necessary for GinkgoCADx
281 pc.acceptedRole != ASC_SC_ROLE_SCP &&
282 pc.acceptedRole != ASC_SC_ROLE_SCUSCP)
283 {
284 // the role is not appropriate
285 nFailed_++;
286 AddFailedUIDInstance(sopInstanceUid);
287 LOG(ERROR) << "C-GET SCP: storeSCU: [No presentation context with requestor SCP role for: ("
288 << dcmSOPClassUIDToModality(sopClassUid.c_str(), "OT") << ") " << sopClassUid;
289 return DIMSE_NOVALIDPRESENTATIONCONTEXTID;
290 }
291 }
292
293 const DIC_US msgId = assoc->nextMsgID++;
294
295 T_DIMSE_C_StoreRQ req;
296 memset(&req, 0, sizeof(req));
297 req.MessageID = msgId;
298 strncpy(req.AffectedSOPClassUID, sopClassUid.c_str(), DIC_UI_LEN);
299 strncpy(req.AffectedSOPInstanceUID, sopInstanceUid.c_str(), DIC_UI_LEN);
300 req.DataSetType = DIMSE_DATASET_PRESENT;
301 req.Priority = DIMSE_PRIORITY_MEDIUM;
302 req.opts = 0;
303
304 T_DIMSE_C_StoreRSP rsp;
305 memset(&rsp, 0, sizeof(rsp));
306
307 LOG(INFO) << "Store SCU RQ: MsgID " << msgId << ", ("
308 << dcmSOPClassUIDToModality(sopClassUid.c_str(), "OT") << ")";
309
310 T_DIMSE_DetectedCancelParameters cancelParameters;
311 memset(&cancelParameters, 0, sizeof(cancelParameters));
312
313 std::unique_ptr<DcmDataset> stDetail;
314
315 OFCondition cond;
316
317 if (sourceSyntax == selectedSyntax)
318 {
319 // No transcoding is required
320 DcmDataset *stDetailTmp = NULL;
321 cond = DIMSE_storeUser(
322 assoc, presId, &req, NULL /* imageFileName */, dicom->getDataset(),
323 GetSubOpProgressCallback, this /* callbackData */,
324 (timeout_ > 0 ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout_,
325 &rsp, &stDetailTmp, &cancelParameters);
326 stDetail.reset(stDetailTmp);
327 }
328 else
329 {
330 // Transcoding to the selected uncompressed transfer syntax
331 IDicomTranscoder::DicomImage source, transcoded;
332 source.AcquireParsed(dicom.release());
333
334 std::set<DicomTransferSyntax> ts;
335 ts.insert(selectedSyntax);
336
337 if (context_.Transcode(transcoded, source, ts, true))
338 {
339 // Transcoding has succeeded
340 DcmDataset *stDetailTmp = NULL;
341 cond = DIMSE_storeUser(
342 assoc, presId, &req, NULL /* imageFileName */,
343 transcoded.GetParsed().getDataset(),
344 GetSubOpProgressCallback, this /* callbackData */,
345 (timeout_ > 0 ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout_,
346 &rsp, &stDetailTmp, &cancelParameters);
347 stDetail.reset(stDetailTmp);
348 }
349 else
350 {
351 // Cannot transcode
352 nFailed_++;
353 AddFailedUIDInstance(sopInstanceUid);
354 LOG(ERROR) << "C-GET SCP: Cannot transcode " << sopClassUid
355 << " from transfer syntax " << GetTransferSyntaxUid(sourceSyntax)
356 << " to " << GetTransferSyntaxUid(selectedSyntax);
357 return DIMSE_NOVALIDPRESENTATIONCONTEXTID;
358 }
359 }
360
361 if (cond.good())
362 {
363 if (cancelParameters.cancelEncountered)
364 {
365 if (origPresId_ == cancelParameters.presId &&
366 origMsgId_ == cancelParameters.req.MessageIDBeingRespondedTo)
367 {
368 getCancelled_ = OFTrue;
369 }
370 else
371 {
372 LOG(ERROR) << "C-GET SCP: Unexpected C-Cancel-RQ encountered: pid=" << (int)cancelParameters.presId
373 << ", mid=" << (int)cancelParameters.req.MessageIDBeingRespondedTo;
374 }
375 }
376
377 if (rsp.DimseStatus == STATUS_Success)
378 {
379 // everything ok
380 nCompleted_++;
381 }
382 else if ((rsp.DimseStatus & 0xf000) == 0xb000)
383 {
384 // a warning status message
385 warningCount_++;
386 LOG(ERROR) << "C-GET SCP: Store Warning: Response Status: "
387 << DU_cstoreStatusString(rsp.DimseStatus);
388 }
389 else
390 {
391 nFailed_++;
392 AddFailedUIDInstance(sopInstanceUid);
393 // print a status message
394 LOG(ERROR) << "C-GET SCP: Store Failed: Response Status: "
395 << DU_cstoreStatusString(rsp.DimseStatus);
396 }
397 }
398 else
399 {
400 nFailed_++;
401 AddFailedUIDInstance(sopInstanceUid);
402 OFString temp_str;
403 LOG(ERROR) << "C-GET SCP: storeSCU: Store Request Failed: " << DimseCondition::dump(temp_str, cond);
404 }
405
406 if (stDetail.get() != NULL)
407 {
408 // It is impossible to directly use the "<<" stream construct
409 // with "DcmObject::PrintHelper" using MSVC2008
410 std::stringstream s;
411 DcmObject::PrintHelper obj(*stDetail);
412 obj.dcmobj_.print(s);
413
414 LOG(INFO) << " Status Detail: " << s.str();
415 }
416
417 return cond;
418 }
419
420 bool OrthancGetRequestHandler::LookupIdentifiers(std::list<std::string>& publicIds,
421 ResourceType level,
422 const DicomMap& input) const
423 {
424 DicomTag tag(0, 0); // Dummy initialization
425
426 switch (level)
427 {
428 case ResourceType_Patient:
429 tag = DICOM_TAG_PATIENT_ID;
430 break;
431
432 case ResourceType_Study:
433 tag = (input.HasTag(DICOM_TAG_ACCESSION_NUMBER) ?
434 DICOM_TAG_ACCESSION_NUMBER : DICOM_TAG_STUDY_INSTANCE_UID);
435 break;
436
437 case ResourceType_Series:
438 tag = DICOM_TAG_SERIES_INSTANCE_UID;
439 break;
440
441 case ResourceType_Instance:
442 tag = DICOM_TAG_SOP_INSTANCE_UID;
443 break;
444
445 default:
446 throw OrthancException(ErrorCode_ParameterOutOfRange);
447 }
448
449 if (!input.HasTag(tag))
450 {
451 return false;
452 }
453
454 const DicomValue& value = input.GetValue(tag);
455 if (value.IsNull() ||
456 value.IsBinary())
457 {
458 return false;
459 }
460 else
461 {
462 std::vector<std::string> tokens;
463 Toolbox::TokenizeString(tokens, value.GetContent(), '\\');
464
465 for (size_t i = 0; i < tokens.size(); i++)
466 {
467 std::vector<std::string> tmp;
468 context_.GetIndex().LookupIdentifierExact(tmp, level, tag, tokens[i]);
469
470 if (tmp.empty())
471 {
472 LOG(ERROR) << "C-GET: Cannot locate resource \"" << tokens[i]
473 << "\" at the " << EnumerationToString(level) << " level";
474 return false;
475 }
476 else
477 {
478 for (size_t i = 0; i < tmp.size(); i++)
479 {
480 publicIds.push_back(tmp[i]);
481 }
482 }
483 }
484
485 return true;
486 }
487 }
488
489
490 OrthancGetRequestHandler::OrthancGetRequestHandler(ServerContext& context) :
491 context_(context)
492 {
493 position_ = 0;
494 nRemaining_ = 0;
495 nCompleted_ = 0;
496 warningCount_ = 0;
497 nFailed_ = 0;
498 timeout_ = 0;
499 }
500
501
502 bool OrthancGetRequestHandler::Handle(const DicomMap& input,
503 const std::string& originatorIp,
504 const std::string& originatorAet,
505 const std::string& calledAet,
506 uint32_t timeout)
507 {
508 MetricsRegistry::Timer timer(context_.GetMetricsRegistry(), "orthanc_get_scp_duration_ms");
509
510 LOG(WARNING) << "C-GET-SCU request received from AET \"" << originatorAet << "\"";
511
512 {
513 DicomArray query(input);
514 for (size_t i = 0; i < query.GetSize(); i++)
515 {
516 if (!query.GetElement(i).GetValue().IsNull())
517 {
518 LOG(INFO) << " " << query.GetElement(i).GetTag()
519 << " " << FromDcmtkBridge::GetTagName(query.GetElement(i))
520 << " = " << query.GetElement(i).GetValue().GetContent();
521 }
522 }
523 }
524
525 /**
526 * Retrieve the query level.
527 **/
528
529 const DicomValue* levelTmp = input.TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL);
530
531 assert(levelTmp != NULL);
532 ResourceType level = StringToResourceType(levelTmp->GetContent().c_str());
533
534
535 /**
536 * Lookup for the resource to be sent.
537 **/
538
539 std::list<std::string> publicIds;
540
541 if (!LookupIdentifiers(publicIds, level, input))
542 {
543 LOG(ERROR) << "Cannot determine what resources are requested by C-GET";
544 return false;
545 }
546
547 localAet_ = context_.GetDefaultLocalApplicationEntityTitle();
548 position_ = 0;
549 originatorAet_ = originatorAet;
550
551 {
552 OrthancConfiguration::ReaderLock lock;
553 remote_ = lock.GetConfiguration().GetModalityUsingAet(originatorAet);
554 }
555
556 for (std::list<std::string>::const_iterator
557 resource = publicIds.begin(); resource != publicIds.end(); ++resource)
558 {
559 LOG(INFO) << "C-GET: Sending resource " << *resource
560 << " to modality \"" << originatorAet << "\"";
561
562 std::list<std::string> tmp;
563 context_.GetIndex().GetChildInstances(tmp, *resource);
564
565 instances_.reserve(tmp.size());
566 for (std::list<std::string>::iterator it = tmp.begin(); it != tmp.end(); ++it)
567 {
568 instances_.push_back(*it);
569 }
570 }
571
572 failedUIDs_.clear();
573 getCancelled_ = OFFalse;
574
575 nRemaining_ = GetSubOperationCount();
576 nCompleted_ = 0;
577 nFailed_ = 0;
578 warningCount_ = 0;
579 timeout_ = timeout;
580
581 return true;
582 }
583 };