comparison OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.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/OrthancRestApi/OrthancRestModalities.cpp@5797ca4f3b8d
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
34 #include "../PrecompiledHeadersServer.h"
35 #include "OrthancRestApi.h"
36
37 #include "../../Core/Cache/SharedArchive.h"
38 #include "../../Core/DicomNetworking/DicomAssociation.h"
39 #include "../../Core/DicomNetworking/DicomControlUserConnection.h"
40 #include "../../Core/DicomParsing/FromDcmtkBridge.h"
41 #include "../../Core/Logging.h"
42 #include "../../Core/SerializationToolbox.h"
43
44 #include "../OrthancConfiguration.h"
45 #include "../QueryRetrieveHandler.h"
46 #include "../ServerContext.h"
47 #include "../ServerJobs/DicomModalityStoreJob.h"
48 #include "../ServerJobs/DicomMoveScuJob.h"
49 #include "../ServerJobs/OrthancPeerStoreJob.h"
50 #include "../ServerToolbox.h"
51 #include "../StorageCommitmentReports.h"
52
53
54 namespace Orthanc
55 {
56 static const char* const KEY_LEVEL = "Level";
57 static const char* const KEY_LOCAL_AET = "LocalAet";
58 static const char* const KEY_NORMALIZE = "Normalize";
59 static const char* const KEY_QUERY = "Query";
60 static const char* const KEY_RESOURCES = "Resources";
61 static const char* const KEY_TARGET_AET = "TargetAet";
62 static const char* const KEY_TIMEOUT = "Timeout";
63 static const char* const SOP_CLASS_UID = "SOPClassUID";
64 static const char* const SOP_INSTANCE_UID = "SOPInstanceUID";
65
66 static RemoteModalityParameters MyGetModalityUsingSymbolicName(const std::string& name)
67 {
68 OrthancConfiguration::ReaderLock lock;
69 return lock.GetConfiguration().GetModalityUsingSymbolicName(name);
70 }
71
72
73 static void InjectAssociationTimeout(DicomAssociationParameters& params,
74 const Json::Value& body)
75 {
76 if (body.type() != Json::objectValue)
77 {
78 throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON object");
79 }
80 else if (body.isMember(KEY_TIMEOUT))
81 {
82 // New in Orthanc 1.7.0
83 params.SetTimeout(SerializationToolbox::ReadUnsignedInteger(body, KEY_TIMEOUT));
84 }
85 }
86
87 static DicomAssociationParameters GetAssociationParameters(RestApiPostCall& call,
88 const Json::Value& body)
89 {
90 const std::string& localAet =
91 OrthancRestApi::GetContext(call).GetDefaultLocalApplicationEntityTitle();
92 const RemoteModalityParameters remote =
93 MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
94
95 DicomAssociationParameters params(localAet, remote);
96 InjectAssociationTimeout(params, body);
97
98 return params;
99 }
100
101
102 static DicomAssociationParameters GetAssociationParameters(RestApiPostCall& call)
103 {
104 Json::Value body;
105 call.ParseJsonRequest(body);
106 return GetAssociationParameters(call, body);
107 }
108
109
110 /***************************************************************************
111 * DICOM C-Echo SCU
112 ***************************************************************************/
113
114 static void DicomEcho(RestApiPostCall& call)
115 {
116 DicomControlUserConnection connection(GetAssociationParameters(call));
117
118 if (connection.Echo())
119 {
120 // Echo has succeeded
121 call.GetOutput().AnswerBuffer("{}", MimeType_Json);
122 return;
123 }
124 else
125 {
126 // Echo has failed
127 call.GetOutput().SignalError(HttpStatus_500_InternalServerError);
128 }
129 }
130
131
132
133 /***************************************************************************
134 * DICOM C-Find SCU => DEPRECATED!
135 ***************************************************************************/
136
137 static bool MergeQueryAndTemplate(DicomMap& result,
138 const RestApiCall& call)
139 {
140 Json::Value query;
141
142 if (!call.ParseJsonRequest(query) ||
143 query.type() != Json::objectValue)
144 {
145 return false;
146 }
147
148 Json::Value::Members members = query.getMemberNames();
149 for (size_t i = 0; i < members.size(); i++)
150 {
151 DicomTag t = FromDcmtkBridge::ParseTag(members[i]);
152 result.SetValue(t, query[members[i]].asString(), false);
153 }
154
155 return true;
156 }
157
158
159 static void FindPatient(DicomFindAnswers& result,
160 DicomControlUserConnection& connection,
161 const DicomMap& fields)
162 {
163 // Only keep the filters from "fields" that are related to the patient
164 DicomMap s;
165 fields.ExtractPatientInformation(s);
166 connection.Find(result, ResourceType_Patient, s, true /* normalize */);
167 }
168
169
170 static void FindStudy(DicomFindAnswers& result,
171 DicomControlUserConnection& connection,
172 const DicomMap& fields)
173 {
174 // Only keep the filters from "fields" that are related to the study
175 DicomMap s;
176 fields.ExtractStudyInformation(s);
177
178 s.CopyTagIfExists(fields, DICOM_TAG_PATIENT_ID);
179 s.CopyTagIfExists(fields, DICOM_TAG_ACCESSION_NUMBER);
180 s.CopyTagIfExists(fields, DICOM_TAG_MODALITIES_IN_STUDY);
181
182 connection.Find(result, ResourceType_Study, s, true /* normalize */);
183 }
184
185 static void FindSeries(DicomFindAnswers& result,
186 DicomControlUserConnection& connection,
187 const DicomMap& fields)
188 {
189 // Only keep the filters from "fields" that are related to the series
190 DicomMap s;
191 fields.ExtractSeriesInformation(s);
192
193 s.CopyTagIfExists(fields, DICOM_TAG_PATIENT_ID);
194 s.CopyTagIfExists(fields, DICOM_TAG_ACCESSION_NUMBER);
195 s.CopyTagIfExists(fields, DICOM_TAG_STUDY_INSTANCE_UID);
196
197 connection.Find(result, ResourceType_Series, s, true /* normalize */);
198 }
199
200 static void FindInstance(DicomFindAnswers& result,
201 DicomControlUserConnection& connection,
202 const DicomMap& fields)
203 {
204 // Only keep the filters from "fields" that are related to the instance
205 DicomMap s;
206 fields.ExtractInstanceInformation(s);
207
208 s.CopyTagIfExists(fields, DICOM_TAG_PATIENT_ID);
209 s.CopyTagIfExists(fields, DICOM_TAG_ACCESSION_NUMBER);
210 s.CopyTagIfExists(fields, DICOM_TAG_STUDY_INSTANCE_UID);
211 s.CopyTagIfExists(fields, DICOM_TAG_SERIES_INSTANCE_UID);
212
213 connection.Find(result, ResourceType_Instance, s, true /* normalize */);
214 }
215
216
217 static void DicomFindPatient(RestApiPostCall& call)
218 {
219 LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri();
220
221 DicomMap fields;
222 DicomMap::SetupFindPatientTemplate(fields);
223 if (!MergeQueryAndTemplate(fields, call))
224 {
225 return;
226 }
227
228 DicomFindAnswers answers(false);
229
230 {
231 DicomControlUserConnection connection(GetAssociationParameters(call));
232 FindPatient(answers, connection, fields);
233 }
234
235 Json::Value result;
236 answers.ToJson(result, true);
237 call.GetOutput().AnswerJson(result);
238 }
239
240 static void DicomFindStudy(RestApiPostCall& call)
241 {
242 LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri();
243
244 DicomMap fields;
245 DicomMap::SetupFindStudyTemplate(fields);
246 if (!MergeQueryAndTemplate(fields, call))
247 {
248 return;
249 }
250
251 if (fields.GetValue(DICOM_TAG_ACCESSION_NUMBER).GetContent().size() <= 2 &&
252 fields.GetValue(DICOM_TAG_PATIENT_ID).GetContent().size() <= 2)
253 {
254 return;
255 }
256
257 DicomFindAnswers answers(false);
258
259 {
260 DicomControlUserConnection connection(GetAssociationParameters(call));
261 FindStudy(answers, connection, fields);
262 }
263
264 Json::Value result;
265 answers.ToJson(result, true);
266 call.GetOutput().AnswerJson(result);
267 }
268
269 static void DicomFindSeries(RestApiPostCall& call)
270 {
271 LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri();
272
273 DicomMap fields;
274 DicomMap::SetupFindSeriesTemplate(fields);
275 if (!MergeQueryAndTemplate(fields, call))
276 {
277 return;
278 }
279
280 if ((fields.GetValue(DICOM_TAG_ACCESSION_NUMBER).GetContent().size() <= 2 &&
281 fields.GetValue(DICOM_TAG_PATIENT_ID).GetContent().size() <= 2) ||
282 fields.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).GetContent().size() <= 2)
283 {
284 return;
285 }
286
287 DicomFindAnswers answers(false);
288
289 {
290 DicomControlUserConnection connection(GetAssociationParameters(call));
291 FindSeries(answers, connection, fields);
292 }
293
294 Json::Value result;
295 answers.ToJson(result, true);
296 call.GetOutput().AnswerJson(result);
297 }
298
299 static void DicomFindInstance(RestApiPostCall& call)
300 {
301 LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri();
302
303 DicomMap fields;
304 DicomMap::SetupFindInstanceTemplate(fields);
305 if (!MergeQueryAndTemplate(fields, call))
306 {
307 return;
308 }
309
310 if ((fields.GetValue(DICOM_TAG_ACCESSION_NUMBER).GetContent().size() <= 2 &&
311 fields.GetValue(DICOM_TAG_PATIENT_ID).GetContent().size() <= 2) ||
312 fields.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).GetContent().size() <= 2 ||
313 fields.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).GetContent().size() <= 2)
314 {
315 return;
316 }
317
318 DicomFindAnswers answers(false);
319
320 {
321 DicomControlUserConnection connection(GetAssociationParameters(call));
322 FindInstance(answers, connection, fields);
323 }
324
325 Json::Value result;
326 answers.ToJson(result, true);
327 call.GetOutput().AnswerJson(result);
328 }
329
330
331 static void CopyTagIfExists(DicomMap& target,
332 ParsedDicomFile& source,
333 const DicomTag& tag)
334 {
335 std::string tmp;
336 if (source.GetTagValue(tmp, tag))
337 {
338 target.SetValue(tag, tmp, false);
339 }
340 }
341
342
343 static void DicomFind(RestApiPostCall& call)
344 {
345 LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri();
346
347 DicomMap m;
348 DicomMap::SetupFindPatientTemplate(m);
349 if (!MergeQueryAndTemplate(m, call))
350 {
351 return;
352 }
353
354 DicomControlUserConnection connection(GetAssociationParameters(call));
355
356 DicomFindAnswers patients(false);
357 FindPatient(patients, connection, m);
358
359 // Loop over the found patients
360 Json::Value result = Json::arrayValue;
361 for (size_t i = 0; i < patients.GetSize(); i++)
362 {
363 Json::Value patient;
364 patients.ToJson(patient, i, true);
365
366 DicomMap::SetupFindStudyTemplate(m);
367 if (!MergeQueryAndTemplate(m, call))
368 {
369 return;
370 }
371
372 CopyTagIfExists(m, patients.GetAnswer(i), DICOM_TAG_PATIENT_ID);
373
374 DicomFindAnswers studies(false);
375 FindStudy(studies, connection, m);
376
377 patient["Studies"] = Json::arrayValue;
378
379 // Loop over the found studies
380 for (size_t j = 0; j < studies.GetSize(); j++)
381 {
382 Json::Value study;
383 studies.ToJson(study, j, true);
384
385 DicomMap::SetupFindSeriesTemplate(m);
386 if (!MergeQueryAndTemplate(m, call))
387 {
388 return;
389 }
390
391 CopyTagIfExists(m, studies.GetAnswer(j), DICOM_TAG_PATIENT_ID);
392 CopyTagIfExists(m, studies.GetAnswer(j), DICOM_TAG_STUDY_INSTANCE_UID);
393
394 DicomFindAnswers series(false);
395 FindSeries(series, connection, m);
396
397 // Loop over the found series
398 study["Series"] = Json::arrayValue;
399 for (size_t k = 0; k < series.GetSize(); k++)
400 {
401 Json::Value series2;
402 series.ToJson(series2, k, true);
403 study["Series"].append(series2);
404 }
405
406 patient["Studies"].append(study);
407 }
408
409 result.append(patient);
410 }
411
412 call.GetOutput().AnswerJson(result);
413 }
414
415
416
417 /***************************************************************************
418 * DICOM C-Find and C-Move SCU => Recommended since Orthanc 0.9.0
419 ***************************************************************************/
420
421 static void AnswerQueryHandler(RestApiPostCall& call,
422 std::unique_ptr<QueryRetrieveHandler>& handler)
423 {
424 ServerContext& context = OrthancRestApi::GetContext(call);
425
426 if (handler.get() == NULL)
427 {
428 throw OrthancException(ErrorCode_NullPointer);
429 }
430
431 handler->Run();
432
433 std::string s = context.GetQueryRetrieveArchive().Add(handler.release());
434 Json::Value result = Json::objectValue;
435 result["ID"] = s;
436 result["Path"] = "/queries/" + s;
437
438 call.GetOutput().AnswerJson(result);
439 }
440
441
442 static void DicomQuery(RestApiPostCall& call)
443 {
444 ServerContext& context = OrthancRestApi::GetContext(call);
445 Json::Value request;
446
447 if (!call.ParseJsonRequest(request) ||
448 request.type() != Json::objectValue)
449 {
450 throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON object");
451 }
452 else if (!request.isMember(KEY_LEVEL) ||
453 request[KEY_LEVEL].type() != Json::stringValue)
454 {
455 throw OrthancException(ErrorCode_BadFileFormat,
456 "The JSON body must contain field " + std::string(KEY_LEVEL));
457 }
458 else if (request.isMember(KEY_NORMALIZE) &&
459 request[KEY_NORMALIZE].type() != Json::booleanValue)
460 {
461 throw OrthancException(ErrorCode_BadFileFormat,
462 "The field " + std::string(KEY_NORMALIZE) + " must contain a Boolean");
463 }
464 else if (request.isMember(KEY_QUERY) &&
465 request[KEY_QUERY].type() != Json::objectValue)
466 {
467 throw OrthancException(ErrorCode_BadFileFormat,
468 "The field " + std::string(KEY_QUERY) + " must contain a JSON object");
469 }
470 else
471 {
472 std::unique_ptr<QueryRetrieveHandler> handler(new QueryRetrieveHandler(context));
473
474 handler->SetModality(call.GetUriComponent("id", ""));
475 handler->SetLevel(StringToResourceType(request[KEY_LEVEL].asCString()));
476
477 if (request.isMember(KEY_QUERY))
478 {
479 std::map<DicomTag, std::string> query;
480 SerializationToolbox::ReadMapOfTags(query, request, KEY_QUERY);
481
482 for (std::map<DicomTag, std::string>::const_iterator
483 it = query.begin(); it != query.end(); ++it)
484 {
485 handler->SetQuery(it->first, it->second);
486 }
487 }
488
489 if (request.isMember(KEY_NORMALIZE))
490 {
491 handler->SetFindNormalized(request[KEY_NORMALIZE].asBool());
492 }
493
494 AnswerQueryHandler(call, handler);
495 }
496 }
497
498
499 static void ListQueries(RestApiGetCall& call)
500 {
501 ServerContext& context = OrthancRestApi::GetContext(call);
502
503 std::list<std::string> queries;
504 context.GetQueryRetrieveArchive().List(queries);
505
506 Json::Value result = Json::arrayValue;
507 for (std::list<std::string>::const_iterator
508 it = queries.begin(); it != queries.end(); ++it)
509 {
510 result.append(*it);
511 }
512
513 call.GetOutput().AnswerJson(result);
514 }
515
516
517 namespace
518 {
519 class QueryAccessor
520 {
521 private:
522 ServerContext& context_;
523 SharedArchive::Accessor accessor_;
524 QueryRetrieveHandler* handler_;
525
526 public:
527 QueryAccessor(RestApiCall& call) :
528 context_(OrthancRestApi::GetContext(call)),
529 accessor_(context_.GetQueryRetrieveArchive(), call.GetUriComponent("id", "")),
530 handler_(NULL)
531 {
532 if (accessor_.IsValid())
533 {
534 handler_ = &dynamic_cast<QueryRetrieveHandler&>(accessor_.GetItem());
535 }
536 else
537 {
538 throw OrthancException(ErrorCode_UnknownResource);
539 }
540 }
541
542 QueryRetrieveHandler& GetHandler() const
543 {
544 assert(handler_ != NULL);
545 return *handler_;
546 }
547 };
548
549 static void AnswerDicomMap(RestApiCall& call,
550 const DicomMap& value,
551 bool simplify)
552 {
553 Json::Value full = Json::objectValue;
554 FromDcmtkBridge::ToJson(full, value, simplify);
555 call.GetOutput().AnswerJson(full);
556 }
557 }
558
559
560 static void ListQueryAnswers(RestApiGetCall& call)
561 {
562 const bool expand = call.HasArgument("expand");
563 const bool simplify = call.HasArgument("simplify");
564
565 QueryAccessor query(call);
566 size_t count = query.GetHandler().GetAnswersCount();
567
568 Json::Value result = Json::arrayValue;
569 for (size_t i = 0; i < count; i++)
570 {
571 if (expand)
572 {
573 // New in Orthanc 1.5.0
574 DicomMap value;
575 query.GetHandler().GetAnswer(value, i);
576
577 Json::Value json = Json::objectValue;
578 FromDcmtkBridge::ToJson(json, value, simplify);
579
580 result.append(json);
581 }
582 else
583 {
584 result.append(boost::lexical_cast<std::string>(i));
585 }
586 }
587
588 call.GetOutput().AnswerJson(result);
589 }
590
591
592 static void GetQueryOneAnswer(RestApiGetCall& call)
593 {
594 size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", ""));
595
596 QueryAccessor query(call);
597
598 DicomMap map;
599 query.GetHandler().GetAnswer(map, index);
600
601 AnswerDicomMap(call, map, call.HasArgument("simplify"));
602 }
603
604
605 static void SubmitRetrieveJob(RestApiPostCall& call,
606 bool allAnswers,
607 size_t index)
608 {
609 ServerContext& context = OrthancRestApi::GetContext(call);
610
611 std::string targetAet;
612 int timeout = -1;
613
614 Json::Value body;
615 if (call.ParseJsonRequest(body))
616 {
617 targetAet = Toolbox::GetJsonStringField(body, KEY_TARGET_AET, context.GetDefaultLocalApplicationEntityTitle());
618 timeout = Toolbox::GetJsonIntegerField(body, KEY_TIMEOUT, -1);
619 }
620 else
621 {
622 body = Json::objectValue;
623 if (call.GetBodySize() > 0)
624 {
625 call.BodyToString(targetAet);
626 }
627 else
628 {
629 targetAet = context.GetDefaultLocalApplicationEntityTitle();
630 }
631 }
632
633 std::unique_ptr<DicomMoveScuJob> job(new DicomMoveScuJob(context));
634
635 {
636 QueryAccessor query(call);
637 job->SetTargetAet(targetAet);
638 job->SetLocalAet(query.GetHandler().GetLocalAet());
639 job->SetRemoteModality(query.GetHandler().GetRemoteModality());
640
641 if (timeout >= 0)
642 {
643 // New in Orthanc 1.7.0
644 job->SetTimeout(static_cast<uint32_t>(timeout));
645 }
646
647 LOG(WARNING) << "Driving C-Move SCU on remote modality "
648 << query.GetHandler().GetRemoteModality().GetApplicationEntityTitle()
649 << " to target modality " << targetAet;
650
651 if (allAnswers)
652 {
653 for (size_t i = 0; i < query.GetHandler().GetAnswersCount(); i++)
654 {
655 job->AddFindAnswer(query.GetHandler(), i);
656 }
657 }
658 else
659 {
660 job->AddFindAnswer(query.GetHandler(), index);
661 }
662 }
663
664 OrthancRestApi::GetApi(call).SubmitCommandsJob
665 (call, job.release(), true /* synchronous by default */, body);
666 }
667
668
669 static void RetrieveOneAnswer(RestApiPostCall& call)
670 {
671 size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", ""));
672 SubmitRetrieveJob(call, false, index);
673 }
674
675
676 static void RetrieveAllAnswers(RestApiPostCall& call)
677 {
678 SubmitRetrieveJob(call, true, 0);
679 }
680
681
682 static void GetQueryArguments(RestApiGetCall& call)
683 {
684 QueryAccessor query(call);
685 AnswerDicomMap(call, query.GetHandler().GetQuery(), call.HasArgument("simplify"));
686 }
687
688
689 static void GetQueryLevel(RestApiGetCall& call)
690 {
691 QueryAccessor query(call);
692 call.GetOutput().AnswerBuffer(EnumerationToString(query.GetHandler().GetLevel()), MimeType_PlainText);
693 }
694
695
696 static void GetQueryModality(RestApiGetCall& call)
697 {
698 QueryAccessor query(call);
699 call.GetOutput().AnswerBuffer(query.GetHandler().GetModalitySymbolicName(), MimeType_PlainText);
700 }
701
702
703 static void DeleteQuery(RestApiDeleteCall& call)
704 {
705 ServerContext& context = OrthancRestApi::GetContext(call);
706 context.GetQueryRetrieveArchive().Remove(call.GetUriComponent("id", ""));
707 call.GetOutput().AnswerBuffer("", MimeType_PlainText);
708 }
709
710
711 static void ListQueryOperations(RestApiGetCall& call)
712 {
713 // Ensure that the query of interest does exist
714 QueryAccessor query(call);
715
716 RestApi::AutoListChildren(call);
717 }
718
719
720 static void ListQueryAnswerOperations(RestApiGetCall& call)
721 {
722 // Ensure that the query of interest does exist
723 QueryAccessor query(call);
724
725 // Ensure that the answer of interest does exist
726 size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", ""));
727
728 DicomMap map;
729 query.GetHandler().GetAnswer(map, index);
730
731 Json::Value answer = Json::arrayValue;
732 answer.append("content");
733 answer.append("retrieve");
734
735 switch (query.GetHandler().GetLevel())
736 {
737 case ResourceType_Patient:
738 answer.append("query-study");
739
740 case ResourceType_Study:
741 answer.append("query-series");
742
743 case ResourceType_Series:
744 answer.append("query-instances");
745 break;
746
747 default:
748 break;
749 }
750
751 call.GetOutput().AnswerJson(answer);
752 }
753
754
755 template <ResourceType CHILDREN_LEVEL>
756 static void QueryAnswerChildren(RestApiPostCall& call)
757 {
758 // New in Orthanc 1.5.0
759 assert(CHILDREN_LEVEL == ResourceType_Study ||
760 CHILDREN_LEVEL == ResourceType_Series ||
761 CHILDREN_LEVEL == ResourceType_Instance);
762
763 ServerContext& context = OrthancRestApi::GetContext(call);
764
765 std::unique_ptr<QueryRetrieveHandler> handler(new QueryRetrieveHandler(context));
766
767 {
768 const QueryAccessor parent(call);
769 const ResourceType level = parent.GetHandler().GetLevel();
770
771 const size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", ""));
772
773 Json::Value request;
774
775 if (index >= parent.GetHandler().GetAnswersCount())
776 {
777 throw OrthancException(ErrorCode_ParameterOutOfRange);
778 }
779 else if (CHILDREN_LEVEL == ResourceType_Study &&
780 level != ResourceType_Patient)
781 {
782 throw OrthancException(ErrorCode_UnknownResource);
783 }
784 else if (CHILDREN_LEVEL == ResourceType_Series &&
785 level != ResourceType_Patient &&
786 level != ResourceType_Study)
787 {
788 throw OrthancException(ErrorCode_UnknownResource);
789 }
790 else if (CHILDREN_LEVEL == ResourceType_Instance &&
791 level != ResourceType_Patient &&
792 level != ResourceType_Study &&
793 level != ResourceType_Series)
794 {
795 throw OrthancException(ErrorCode_UnknownResource);
796 }
797 else if (!call.ParseJsonRequest(request))
798 {
799 throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON object");
800 }
801 else
802 {
803 handler->SetFindNormalized(parent.GetHandler().IsFindNormalized());
804 handler->SetModality(parent.GetHandler().GetModalitySymbolicName());
805 handler->SetLevel(CHILDREN_LEVEL);
806
807 if (request.isMember(KEY_QUERY))
808 {
809 std::map<DicomTag, std::string> query;
810 SerializationToolbox::ReadMapOfTags(query, request, KEY_QUERY);
811
812 for (std::map<DicomTag, std::string>::const_iterator
813 it = query.begin(); it != query.end(); ++it)
814 {
815 handler->SetQuery(it->first, it->second);
816 }
817 }
818
819 DicomMap answer;
820 parent.GetHandler().GetAnswer(answer, index);
821
822 // This switch-case mimics "DicomControlUserConnection::Move()"
823 switch (parent.GetHandler().GetLevel())
824 {
825 case ResourceType_Patient:
826 handler->CopyStringTag(answer, DICOM_TAG_PATIENT_ID);
827 break;
828
829 case ResourceType_Study:
830 handler->CopyStringTag(answer, DICOM_TAG_STUDY_INSTANCE_UID);
831 break;
832
833 case ResourceType_Series:
834 handler->CopyStringTag(answer, DICOM_TAG_STUDY_INSTANCE_UID);
835 handler->CopyStringTag(answer, DICOM_TAG_SERIES_INSTANCE_UID);
836 break;
837
838 case ResourceType_Instance:
839 handler->CopyStringTag(answer, DICOM_TAG_STUDY_INSTANCE_UID);
840 handler->CopyStringTag(answer, DICOM_TAG_SERIES_INSTANCE_UID);
841 handler->CopyStringTag(answer, DICOM_TAG_SOP_INSTANCE_UID);
842 break;
843
844 default:
845 throw OrthancException(ErrorCode_InternalError);
846 }
847 }
848 }
849
850 AnswerQueryHandler(call, handler);
851 }
852
853
854
855 /***************************************************************************
856 * DICOM C-Store SCU
857 ***************************************************************************/
858
859 static void GetInstancesToExport(Json::Value& otherArguments,
860 SetOfInstancesJob& job,
861 const std::string& remote,
862 RestApiPostCall& call)
863 {
864 otherArguments = Json::objectValue;
865 ServerContext& context = OrthancRestApi::GetContext(call);
866
867 Json::Value request;
868 if (Toolbox::IsSHA1(call.GetBodyData(), call.GetBodySize()))
869 {
870 std::string s;
871 call.BodyToString(s);
872
873 // This is for compatibility with Orthanc <= 0.5.1.
874 request = Json::arrayValue;
875 request.append(Toolbox::StripSpaces(s));
876 }
877 else if (!call.ParseJsonRequest(request))
878 {
879 // Bad JSON request
880 throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON value");
881 }
882
883 if (request.isString())
884 {
885 std::string item = request.asString();
886 request = Json::arrayValue;
887 request.append(item);
888 }
889 else if (!request.isArray() &&
890 !request.isObject())
891 {
892 throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON object, or a JSON array of strings");
893 }
894
895 const Json::Value* resources;
896 if (request.isArray())
897 {
898 resources = &request;
899 }
900 else
901 {
902 if (request.type() != Json::objectValue ||
903 !request.isMember(KEY_RESOURCES))
904 {
905 throw OrthancException(ErrorCode_BadFileFormat,
906 "Missing field in JSON: \"" + std::string(KEY_RESOURCES) + "\"");
907 }
908
909 resources = &request[KEY_RESOURCES];
910 if (!resources->isArray())
911 {
912 throw OrthancException(ErrorCode_BadFileFormat,
913 "JSON field \"" + std::string(KEY_RESOURCES) + "\" must contain an array");
914 }
915
916 // Copy the remaining arguments
917 Json::Value::Members members = request.getMemberNames();
918 for (Json::Value::ArrayIndex i = 0; i < members.size(); i++)
919 {
920 otherArguments[members[i]] = request[members[i]];
921 }
922 }
923
924 bool logExportedResources;
925
926 {
927 OrthancConfiguration::ReaderLock lock;
928 logExportedResources = lock.GetConfiguration().GetBooleanParameter("LogExportedResources", false);
929 }
930
931 for (Json::Value::ArrayIndex i = 0; i < resources->size(); i++)
932 {
933 if (!(*resources) [i].isString())
934 {
935 throw OrthancException(ErrorCode_BadFileFormat,
936 "Resources to be exported must be specified as a JSON array of strings");
937 }
938
939 std::string stripped = Toolbox::StripSpaces((*resources) [i].asString());
940 if (!Toolbox::IsSHA1(stripped))
941 {
942 throw OrthancException(ErrorCode_BadFileFormat,
943 "This string is not a valid Orthanc identifier: " + stripped);
944 }
945
946 job.AddParentResource(stripped); // New in Orthanc 1.5.7
947
948 context.AddChildInstances(job, stripped);
949
950 if (logExportedResources)
951 {
952 context.GetIndex().LogExportedResource(stripped, remote);
953 }
954 }
955 }
956
957
958 static void DicomStore(RestApiPostCall& call)
959 {
960 ServerContext& context = OrthancRestApi::GetContext(call);
961
962 std::string remote = call.GetUriComponent("id", "");
963
964 Json::Value request;
965 std::unique_ptr<DicomModalityStoreJob> job(new DicomModalityStoreJob(context));
966
967 GetInstancesToExport(request, *job, remote, call);
968
969 std::string localAet = Toolbox::GetJsonStringField
970 (request, KEY_LOCAL_AET, context.GetDefaultLocalApplicationEntityTitle());
971 std::string moveOriginatorAET = Toolbox::GetJsonStringField
972 (request, "MoveOriginatorAet", context.GetDefaultLocalApplicationEntityTitle());
973 int moveOriginatorID = Toolbox::GetJsonIntegerField
974 (request, "MoveOriginatorID", 0 /* By default, not a C-MOVE */);
975
976 job->SetLocalAet(localAet);
977 job->SetRemoteModality(MyGetModalityUsingSymbolicName(remote));
978
979 if (moveOriginatorID != 0)
980 {
981 job->SetMoveOriginator(moveOriginatorAET, moveOriginatorID);
982 }
983
984 // New in Orthanc 1.6.0
985 if (Toolbox::GetJsonBooleanField(request, "StorageCommitment", false))
986 {
987 job->EnableStorageCommitment(true);
988 }
989
990 // New in Orthanc 1.7.0
991 if (request.isMember(KEY_TIMEOUT))
992 {
993 job->SetTimeout(SerializationToolbox::ReadUnsignedInteger(request, KEY_TIMEOUT));
994 }
995
996 OrthancRestApi::GetApi(call).SubmitCommandsJob
997 (call, job.release(), true /* synchronous by default */, request);
998 }
999
1000
1001 static void DicomStoreStraight(RestApiPostCall& call)
1002 {
1003 Json::Value body = Json::objectValue; // No body
1004 DicomStoreUserConnection connection(GetAssociationParameters(call, body));
1005
1006 std::string sopClassUid, sopInstanceUid;
1007 connection.Store(sopClassUid, sopInstanceUid, call.GetBodyData(),
1008 call.GetBodySize(), false /* Not a C-MOVE */, "", 0);
1009
1010 Json::Value answer = Json::objectValue;
1011 answer[SOP_CLASS_UID] = sopClassUid;
1012 answer[SOP_INSTANCE_UID] = sopInstanceUid;
1013
1014 call.GetOutput().AnswerJson(answer);
1015 }
1016
1017
1018 /***************************************************************************
1019 * DICOM C-Move SCU
1020 ***************************************************************************/
1021
1022 static void DicomMove(RestApiPostCall& call)
1023 {
1024 ServerContext& context = OrthancRestApi::GetContext(call);
1025
1026 Json::Value request;
1027
1028 if (!call.ParseJsonRequest(request) ||
1029 request.type() != Json::objectValue ||
1030 !request.isMember(KEY_RESOURCES) ||
1031 !request.isMember(KEY_LEVEL) ||
1032 request[KEY_RESOURCES].type() != Json::arrayValue ||
1033 request[KEY_LEVEL].type() != Json::stringValue)
1034 {
1035 throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON body containing fields " +
1036 std::string(KEY_RESOURCES) + " and " + std::string(KEY_LEVEL));
1037 }
1038
1039 ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString());
1040
1041 std::string localAet = Toolbox::GetJsonStringField
1042 (request, KEY_LOCAL_AET, context.GetDefaultLocalApplicationEntityTitle());
1043 std::string targetAet = Toolbox::GetJsonStringField
1044 (request, KEY_TARGET_AET, context.GetDefaultLocalApplicationEntityTitle());
1045
1046 const RemoteModalityParameters source =
1047 MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
1048
1049 DicomAssociationParameters params(localAet, source);
1050 InjectAssociationTimeout(params, request);
1051
1052 DicomControlUserConnection connection(params);
1053
1054 for (Json::Value::ArrayIndex i = 0; i < request[KEY_RESOURCES].size(); i++)
1055 {
1056 DicomMap resource;
1057 FromDcmtkBridge::FromJson(resource, request[KEY_RESOURCES][i]);
1058
1059 connection.Move(targetAet, level, resource);
1060 }
1061
1062 // Move has succeeded
1063 call.GetOutput().AnswerBuffer("{}", MimeType_Json);
1064 }
1065
1066
1067
1068 /***************************************************************************
1069 * Orthanc Peers => Store client
1070 ***************************************************************************/
1071
1072 static bool IsExistingPeer(const OrthancRestApi::SetOfStrings& peers,
1073 const std::string& id)
1074 {
1075 return peers.find(id) != peers.end();
1076 }
1077
1078 static void ListPeers(RestApiGetCall& call)
1079 {
1080 OrthancConfiguration::ReaderLock lock;
1081
1082 OrthancRestApi::SetOfStrings peers;
1083 lock.GetConfiguration().GetListOfOrthancPeers(peers);
1084
1085 if (call.HasArgument("expand"))
1086 {
1087 Json::Value result = Json::objectValue;
1088 for (OrthancRestApi::SetOfStrings::const_iterator
1089 it = peers.begin(); it != peers.end(); ++it)
1090 {
1091 WebServiceParameters peer;
1092
1093 if (lock.GetConfiguration().LookupOrthancPeer(peer, *it))
1094 {
1095 Json::Value info;
1096 peer.FormatPublic(info);
1097 result[*it] = info;
1098 }
1099 }
1100 call.GetOutput().AnswerJson(result);
1101 }
1102 else // if expand is not present, keep backward compatibility and return an array of peers
1103 {
1104 Json::Value result = Json::arrayValue;
1105 for (OrthancRestApi::SetOfStrings::const_iterator
1106 it = peers.begin(); it != peers.end(); ++it)
1107 {
1108 result.append(*it);
1109 }
1110
1111 call.GetOutput().AnswerJson(result);
1112 }
1113 }
1114
1115 static void ListPeerOperations(RestApiGetCall& call)
1116 {
1117 OrthancConfiguration::ReaderLock lock;
1118
1119 OrthancRestApi::SetOfStrings peers;
1120 lock.GetConfiguration().GetListOfOrthancPeers(peers);
1121
1122 std::string id = call.GetUriComponent("id", "");
1123 if (IsExistingPeer(peers, id))
1124 {
1125 RestApi::AutoListChildren(call);
1126 }
1127 }
1128
1129 static void PeerStore(RestApiPostCall& call)
1130 {
1131 ServerContext& context = OrthancRestApi::GetContext(call);
1132
1133 std::string remote = call.GetUriComponent("id", "");
1134
1135 Json::Value request;
1136 std::unique_ptr<OrthancPeerStoreJob> job(new OrthancPeerStoreJob(context));
1137
1138 GetInstancesToExport(request, *job, remote, call);
1139
1140 static const char* TRANSCODE = "Transcode";
1141 if (request.type() == Json::objectValue &&
1142 request.isMember(TRANSCODE))
1143 {
1144 job->SetTranscode(SerializationToolbox::ReadString(request, TRANSCODE));
1145 }
1146
1147 {
1148 OrthancConfiguration::ReaderLock lock;
1149
1150 WebServiceParameters peer;
1151 if (lock.GetConfiguration().LookupOrthancPeer(peer, remote))
1152 {
1153 job->SetPeer(peer);
1154 }
1155 else
1156 {
1157 throw OrthancException(ErrorCode_UnknownResource,
1158 "No peer with symbolic name: " + remote);
1159 }
1160 }
1161
1162 OrthancRestApi::GetApi(call).SubmitCommandsJob
1163 (call, job.release(), true /* synchronous by default */, request);
1164 }
1165
1166 static void PeerSystem(RestApiGetCall& call)
1167 {
1168 std::string remote = call.GetUriComponent("id", "");
1169
1170 OrthancConfiguration::ReaderLock lock;
1171
1172 WebServiceParameters peer;
1173 if (lock.GetConfiguration().LookupOrthancPeer(peer, remote))
1174 {
1175 HttpClient client(peer, "system");
1176 std::string answer;
1177
1178 client.SetMethod(HttpMethod_Get);
1179
1180 if (!client.Apply(answer))
1181 {
1182 LOG(ERROR) << "Unable to get the system info from remote Orthanc peer: " << peer.GetUrl();
1183 call.GetOutput().SignalError(client.GetLastStatus());
1184 return;
1185 }
1186
1187 call.GetOutput().AnswerBuffer(answer, MimeType_Json);
1188 }
1189 else
1190 {
1191 throw OrthancException(ErrorCode_UnknownResource,
1192 "No peer with symbolic name: " + remote);
1193 }
1194 }
1195
1196 // DICOM bridge -------------------------------------------------------------
1197
1198 static bool IsExistingModality(const OrthancRestApi::SetOfStrings& modalities,
1199 const std::string& id)
1200 {
1201 return modalities.find(id) != modalities.end();
1202 }
1203
1204 static void ListModalities(RestApiGetCall& call)
1205 {
1206 OrthancConfiguration::ReaderLock lock;
1207
1208 OrthancRestApi::SetOfStrings modalities;
1209 lock.GetConfiguration().GetListOfDicomModalities(modalities);
1210
1211 if (call.HasArgument("expand"))
1212 {
1213 Json::Value result = Json::objectValue;
1214 for (OrthancRestApi::SetOfStrings::const_iterator
1215 it = modalities.begin(); it != modalities.end(); ++it)
1216 {
1217 const RemoteModalityParameters& remote = lock.GetConfiguration().GetModalityUsingSymbolicName(*it);
1218
1219 Json::Value info;
1220 remote.Serialize(info, true /* force advanced format */);
1221 result[*it] = info;
1222 }
1223 call.GetOutput().AnswerJson(result);
1224 }
1225 else // if expand is not present, keep backward compatibility and return an array of modalities ids
1226 {
1227 Json::Value result = Json::arrayValue;
1228 for (OrthancRestApi::SetOfStrings::const_iterator
1229 it = modalities.begin(); it != modalities.end(); ++it)
1230 {
1231 result.append(*it);
1232 }
1233 call.GetOutput().AnswerJson(result);
1234 }
1235 }
1236
1237
1238 static void ListModalityOperations(RestApiGetCall& call)
1239 {
1240 OrthancConfiguration::ReaderLock lock;
1241
1242 OrthancRestApi::SetOfStrings modalities;
1243 lock.GetConfiguration().GetListOfDicomModalities(modalities);
1244
1245 std::string id = call.GetUriComponent("id", "");
1246 if (IsExistingModality(modalities, id))
1247 {
1248 RestApi::AutoListChildren(call);
1249 }
1250 }
1251
1252
1253 static void UpdateModality(RestApiPutCall& call)
1254 {
1255 ServerContext& context = OrthancRestApi::GetContext(call);
1256
1257 Json::Value json;
1258 if (call.ParseJsonRequest(json))
1259 {
1260 RemoteModalityParameters modality;
1261 modality.Unserialize(json);
1262
1263 {
1264 OrthancConfiguration::WriterLock lock;
1265 lock.GetConfiguration().UpdateModality(call.GetUriComponent("id", ""), modality);
1266 }
1267
1268 context.SignalUpdatedModalities();
1269
1270 call.GetOutput().AnswerBuffer("", MimeType_PlainText);
1271 }
1272 }
1273
1274
1275 static void DeleteModality(RestApiDeleteCall& call)
1276 {
1277 ServerContext& context = OrthancRestApi::GetContext(call);
1278
1279 {
1280 OrthancConfiguration::WriterLock lock;
1281 lock.GetConfiguration().RemoveModality(call.GetUriComponent("id", ""));
1282 }
1283
1284 context.SignalUpdatedModalities();
1285
1286 call.GetOutput().AnswerBuffer("", MimeType_PlainText);
1287 }
1288
1289
1290 static void UpdatePeer(RestApiPutCall& call)
1291 {
1292 ServerContext& context = OrthancRestApi::GetContext(call);
1293
1294 Json::Value json;
1295 if (call.ParseJsonRequest(json))
1296 {
1297 WebServiceParameters peer;
1298 peer.Unserialize(json);
1299
1300 {
1301 OrthancConfiguration::WriterLock lock;
1302 lock.GetConfiguration().UpdatePeer(call.GetUriComponent("id", ""), peer);
1303 }
1304
1305 context.SignalUpdatedPeers();
1306
1307 call.GetOutput().AnswerBuffer("", MimeType_PlainText);
1308 }
1309 }
1310
1311
1312 static void DeletePeer(RestApiDeleteCall& call)
1313 {
1314 ServerContext& context = OrthancRestApi::GetContext(call);
1315
1316 {
1317 OrthancConfiguration::WriterLock lock;
1318 lock.GetConfiguration().RemovePeer(call.GetUriComponent("id", ""));
1319 }
1320
1321 context.SignalUpdatedPeers();
1322
1323 call.GetOutput().AnswerBuffer("", MimeType_PlainText);
1324 }
1325
1326
1327 static void DicomFindWorklist(RestApiPostCall& call)
1328 {
1329 Json::Value json;
1330 if (call.ParseJsonRequest(json))
1331 {
1332 std::unique_ptr<ParsedDicomFile> query
1333 (ParsedDicomFile::CreateFromJson(json, static_cast<DicomFromJsonFlags>(0),
1334 "" /* no private creator */));
1335
1336 DicomFindAnswers answers(true);
1337
1338 {
1339 DicomControlUserConnection connection(GetAssociationParameters(call, json));
1340 connection.FindWorklist(answers, *query);
1341 }
1342
1343 Json::Value result;
1344 answers.ToJson(result, true);
1345 call.GetOutput().AnswerJson(result);
1346 }
1347 else
1348 {
1349 throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON object");
1350 }
1351 }
1352
1353
1354 // Storage commitment SCU ---------------------------------------------------
1355
1356 static void StorageCommitmentScu(RestApiPostCall& call)
1357 {
1358 static const char* const ORTHANC_RESOURCES = "Resources";
1359 static const char* const DICOM_INSTANCES = "DicomInstances";
1360
1361 ServerContext& context = OrthancRestApi::GetContext(call);
1362
1363 Json::Value json;
1364 if (!call.ParseJsonRequest(json) ||
1365 json.type() != Json::objectValue)
1366 {
1367 throw OrthancException(ErrorCode_BadFileFormat,
1368 "Must provide a JSON object with a list of resources");
1369 }
1370 else if (!json.isMember(ORTHANC_RESOURCES) &&
1371 !json.isMember(DICOM_INSTANCES))
1372 {
1373 throw OrthancException(ErrorCode_BadFileFormat,
1374 "Empty storage commitment request, one of these fields is mandatory: \"" +
1375 std::string(ORTHANC_RESOURCES) + "\" or \"" + std::string(DICOM_INSTANCES) + "\"");
1376 }
1377 else
1378 {
1379 std::list<std::string> sopClassUids, sopInstanceUids;
1380
1381 if (json.isMember(ORTHANC_RESOURCES))
1382 {
1383 const Json::Value& resources = json[ORTHANC_RESOURCES];
1384
1385 if (resources.type() != Json::arrayValue)
1386 {
1387 throw OrthancException(ErrorCode_BadFileFormat,
1388 "The \"" + std::string(ORTHANC_RESOURCES) +
1389 "\" field must provide an array of Orthanc resources");
1390 }
1391 else
1392 {
1393 for (Json::Value::ArrayIndex i = 0; i < resources.size(); i++)
1394 {
1395 if (resources[i].type() != Json::stringValue)
1396 {
1397 throw OrthancException(ErrorCode_BadFileFormat,
1398 "The \"" + std::string(ORTHANC_RESOURCES) +
1399 "\" field must provide an array of strings, found: " + resources[i].toStyledString());
1400 }
1401
1402 std::list<std::string> instances;
1403 context.GetIndex().GetChildInstances(instances, resources[i].asString());
1404
1405 for (std::list<std::string>::const_iterator
1406 it = instances.begin(); it != instances.end(); ++it)
1407 {
1408 std::string sopClassUid, sopInstanceUid;
1409 DicomMap tags;
1410 if (context.LookupOrReconstructMetadata(sopClassUid, *it, MetadataType_Instance_SopClassUid) &&
1411 context.GetIndex().GetAllMainDicomTags(tags, *it) &&
1412 tags.LookupStringValue(sopInstanceUid, DICOM_TAG_SOP_INSTANCE_UID, false))
1413 {
1414 sopClassUids.push_back(sopClassUid);
1415 sopInstanceUids.push_back(sopInstanceUid);
1416 }
1417 else
1418 {
1419 throw OrthancException(ErrorCode_InternalError,
1420 "Cannot retrieve SOP Class/Instance UID of Orthanc instance: " + *it);
1421 }
1422 }
1423 }
1424 }
1425 }
1426
1427 if (json.isMember(DICOM_INSTANCES))
1428 {
1429 const Json::Value& instances = json[DICOM_INSTANCES];
1430
1431 if (instances.type() != Json::arrayValue)
1432 {
1433 throw OrthancException(ErrorCode_BadFileFormat,
1434 "The \"" + std::string(DICOM_INSTANCES) +
1435 "\" field must provide an array of DICOM instances");
1436 }
1437 else
1438 {
1439 for (Json::Value::ArrayIndex i = 0; i < instances.size(); i++)
1440 {
1441 if (instances[i].type() == Json::arrayValue)
1442 {
1443 if (instances[i].size() != 2 ||
1444 instances[i][0].type() != Json::stringValue ||
1445 instances[i][1].type() != Json::stringValue)
1446 {
1447 throw OrthancException(ErrorCode_BadFileFormat,
1448 "An instance entry must provide an array with 2 strings: "
1449 "SOP Class UID and SOP Instance UID");
1450 }
1451 else
1452 {
1453 sopClassUids.push_back(instances[i][0].asString());
1454 sopInstanceUids.push_back(instances[i][1].asString());
1455 }
1456 }
1457 else if (instances[i].type() == Json::objectValue)
1458 {
1459 if (!instances[i].isMember(SOP_CLASS_UID) ||
1460 !instances[i].isMember(SOP_INSTANCE_UID) ||
1461 instances[i][SOP_CLASS_UID].type() != Json::stringValue ||
1462 instances[i][SOP_INSTANCE_UID].type() != Json::stringValue)
1463 {
1464 throw OrthancException(ErrorCode_BadFileFormat,
1465 "An instance entry must provide an object with 2 string fiels: "
1466 "\"" + std::string(SOP_CLASS_UID) + "\" and \"" +
1467 std::string(SOP_INSTANCE_UID));
1468 }
1469 else
1470 {
1471 sopClassUids.push_back(instances[i][SOP_CLASS_UID].asString());
1472 sopInstanceUids.push_back(instances[i][SOP_INSTANCE_UID].asString());
1473 }
1474 }
1475 else
1476 {
1477 throw OrthancException(ErrorCode_BadFileFormat,
1478 "JSON array or object is expected to specify one "
1479 "instance to be queried, found: " + instances[i].toStyledString());
1480 }
1481 }
1482 }
1483 }
1484
1485 if (sopClassUids.size() != sopInstanceUids.size())
1486 {
1487 throw OrthancException(ErrorCode_InternalError);
1488 }
1489
1490 const std::string transactionUid = Toolbox::GenerateDicomPrivateUniqueIdentifier();
1491
1492 if (sopClassUids.empty())
1493 {
1494 LOG(WARNING) << "Issuing an outgoing storage commitment request that is empty: " << transactionUid;
1495 }
1496
1497 {
1498 const RemoteModalityParameters remote =
1499 MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
1500
1501 const std::string& remoteAet = remote.GetApplicationEntityTitle();
1502 const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
1503
1504 // Create a "pending" storage commitment report BEFORE the
1505 // actual SCU call in order to avoid race conditions
1506 context.GetStorageCommitmentReports().Store(
1507 transactionUid, new StorageCommitmentReports::Report(remoteAet));
1508
1509 DicomAssociationParameters parameters(localAet, remote);
1510
1511 std::vector<std::string> a(sopClassUids.begin(), sopClassUids.end());
1512 std::vector<std::string> b(sopInstanceUids.begin(), sopInstanceUids.end());
1513 DicomAssociation::RequestStorageCommitment(parameters, transactionUid, a, b);
1514 }
1515
1516 Json::Value result = Json::objectValue;
1517 result["ID"] = transactionUid;
1518 result["Path"] = "/storage-commitment/" + transactionUid;
1519 call.GetOutput().AnswerJson(result);
1520 }
1521 }
1522
1523
1524 static void GetStorageCommitmentReport(RestApiGetCall& call)
1525 {
1526 ServerContext& context = OrthancRestApi::GetContext(call);
1527
1528 const std::string& transactionUid = call.GetUriComponent("id", "");
1529
1530 {
1531 StorageCommitmentReports::Accessor accessor(
1532 context.GetStorageCommitmentReports(), transactionUid);
1533
1534 if (accessor.IsValid())
1535 {
1536 Json::Value json;
1537 accessor.GetReport().Format(json);
1538 call.GetOutput().AnswerJson(json);
1539 }
1540 else
1541 {
1542 throw OrthancException(ErrorCode_InexistentItem,
1543 "No storage commitment transaction with UID: " + transactionUid);
1544 }
1545 }
1546 }
1547
1548
1549 static void RemoveAfterStorageCommitment(RestApiPostCall& call)
1550 {
1551 ServerContext& context = OrthancRestApi::GetContext(call);
1552
1553 const std::string& transactionUid = call.GetUriComponent("id", "");
1554
1555 {
1556 StorageCommitmentReports::Accessor accessor(
1557 context.GetStorageCommitmentReports(), transactionUid);
1558
1559 if (!accessor.IsValid())
1560 {
1561 throw OrthancException(ErrorCode_InexistentItem,
1562 "No storage commitment transaction with UID: " + transactionUid);
1563 }
1564 else if (accessor.GetReport().GetStatus() != StorageCommitmentReports::Report::Status_Success)
1565 {
1566 throw OrthancException(ErrorCode_BadSequenceOfCalls,
1567 "Cannot remove DICOM instances after failure "
1568 "in storage commitment transaction: " + transactionUid);
1569 }
1570 else
1571 {
1572 std::vector<std::string> sopInstanceUids;
1573 accessor.GetReport().GetSuccessSopInstanceUids(sopInstanceUids);
1574
1575 for (size_t i = 0; i < sopInstanceUids.size(); i++)
1576 {
1577 std::vector<std::string> orthancId;
1578 context.GetIndex().LookupIdentifierExact(
1579 orthancId, ResourceType_Instance, DICOM_TAG_SOP_INSTANCE_UID, sopInstanceUids[i]);
1580
1581 for (size_t j = 0; j < orthancId.size(); j++)
1582 {
1583 LOG(INFO) << "Storage commitment - Removing SOP instance UID / Orthanc ID: "
1584 << sopInstanceUids[i] << " / " << orthancId[j];
1585
1586 Json::Value tmp;
1587 context.GetIndex().DeleteResource(tmp, orthancId[j], ResourceType_Instance);
1588 }
1589 }
1590
1591 call.GetOutput().AnswerBuffer("{}", MimeType_Json);
1592 }
1593 }
1594 }
1595
1596
1597 void OrthancRestApi::RegisterModalities()
1598 {
1599 Register("/modalities", ListModalities);
1600 Register("/modalities/{id}", ListModalityOperations);
1601 Register("/modalities/{id}", UpdateModality);
1602 Register("/modalities/{id}", DeleteModality);
1603 Register("/modalities/{id}/echo", DicomEcho);
1604 Register("/modalities/{id}/find-patient", DicomFindPatient);
1605 Register("/modalities/{id}/find-study", DicomFindStudy);
1606 Register("/modalities/{id}/find-series", DicomFindSeries);
1607 Register("/modalities/{id}/find-instance", DicomFindInstance);
1608 Register("/modalities/{id}/find", DicomFind);
1609 Register("/modalities/{id}/store", DicomStore);
1610 Register("/modalities/{id}/store-straight", DicomStoreStraight); // New in 1.6.1
1611 Register("/modalities/{id}/move", DicomMove);
1612
1613 // For Query/Retrieve
1614 Register("/modalities/{id}/query", DicomQuery);
1615 Register("/queries", ListQueries);
1616 Register("/queries/{id}", DeleteQuery);
1617 Register("/queries/{id}", ListQueryOperations);
1618 Register("/queries/{id}/answers", ListQueryAnswers);
1619 Register("/queries/{id}/answers/{index}", ListQueryAnswerOperations);
1620 Register("/queries/{id}/answers/{index}/content", GetQueryOneAnswer);
1621 Register("/queries/{id}/answers/{index}/retrieve", RetrieveOneAnswer);
1622 Register("/queries/{id}/answers/{index}/query-instances",
1623 QueryAnswerChildren<ResourceType_Instance>);
1624 Register("/queries/{id}/answers/{index}/query-series",
1625 QueryAnswerChildren<ResourceType_Series>);
1626 Register("/queries/{id}/answers/{index}/query-studies",
1627 QueryAnswerChildren<ResourceType_Study>);
1628 Register("/queries/{id}/level", GetQueryLevel);
1629 Register("/queries/{id}/modality", GetQueryModality);
1630 Register("/queries/{id}/query", GetQueryArguments);
1631 Register("/queries/{id}/retrieve", RetrieveAllAnswers);
1632
1633 Register("/peers", ListPeers);
1634 Register("/peers/{id}", ListPeerOperations);
1635 Register("/peers/{id}", UpdatePeer);
1636 Register("/peers/{id}", DeletePeer);
1637 Register("/peers/{id}/store", PeerStore);
1638 Register("/peers/{id}/system", PeerSystem);
1639
1640 Register("/modalities/{id}/find-worklist", DicomFindWorklist);
1641
1642 // Storage commitment
1643 Register("/modalities/{id}/storage-commitment", StorageCommitmentScu);
1644 Register("/storage-commitment/{id}", GetStorageCommitmentReport);
1645 Register("/storage-commitment/{id}/remove", RemoveAfterStorageCommitment);
1646 }
1647 }