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