comparison OrthancServer/OrthancRestApi/OrthancRestApi.cpp @ 750:4afad8cb94fd

moves
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 14 Apr 2014 10:27:53 +0200
parents OrthancServer/OrthancRestApi.cpp@478f4f9de9eb
children 5197fd35333c
comparison
equal deleted inserted replaced
749:b8c49473be38 750:4afad8cb94fd
1 /**
2 * Orthanc - A Lightweight, RESTful DICOM Store
3 * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
4 * Belgium
5 *
6 * This program is free software: you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License as
8 * published by the Free Software Foundation, either version 3 of the
9 * License, or (at your option) any later version.
10 *
11 * In addition, as a special exception, the copyright holders of this
12 * program give permission to link the code of its release with the
13 * OpenSSL project's "OpenSSL" library (or with modified versions of it
14 * that use the same license as the "OpenSSL" library), and distribute
15 * the linked executables. You must obey the GNU General Public License
16 * in all respects for all of the code used other than "OpenSSL". If you
17 * modify file(s) with this exception, you may extend this exception to
18 * your version of the file(s), but you are not obligated to do so. If
19 * you do not wish to do so, delete this exception statement from your
20 * version. If you delete this exception statement from all source files
21 * in the program, then also delete it here.
22 *
23 * This program is distributed in the hope that it will be useful, but
24 * WITHOUT ANY WARRANTY; without even the implied warranty of
25 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
26 * General Public License for more details.
27 *
28 * You should have received a copy of the GNU General Public License
29 * along with this program. If not, see <http://www.gnu.org/licenses/>.
30 **/
31
32
33 #include "OrthancRestApi.h"
34
35 #include "../../Core/Compression/HierarchicalZipWriter.h"
36 #include "../../Core/HttpClient.h"
37 #include "../../Core/HttpServer/FilesystemHttpSender.h"
38 #include "../../Core/Uuid.h"
39 #include "../DicomProtocol/DicomUserConnection.h"
40 #include "../FromDcmtkBridge.h"
41 #include "../OrthancInitialization.h"
42 #include "../ServerToolbox.h"
43
44 #include <dcmtk/dcmdata/dcistrmb.h>
45 #include <dcmtk/dcmdata/dcfilefo.h>
46 #include <boost/lexical_cast.hpp>
47 #include <glog/logging.h>
48
49 #if defined(_MSC_VER)
50 #define snprintf _snprintf
51 #endif
52
53 static const uint64_t MEGA_BYTES = 1024 * 1024;
54 static const uint64_t GIGA_BYTES = 1024 * 1024 * 1024;
55
56
57 namespace Orthanc
58 {
59 // TODO IMPROVE MULTITHREADING
60 // Every call to "ParsedDicomFile" must lock this mutex!!!
61 static boost::mutex cacheMutex_;
62
63
64 // DICOM SCU ----------------------------------------------------------------
65
66 static bool MergeQueryAndTemplate(DicomMap& result,
67 const std::string& postData)
68 {
69 Json::Value query;
70 Json::Reader reader;
71
72 if (!reader.parse(postData, query) ||
73 query.type() != Json::objectValue)
74 {
75 return false;
76 }
77
78 Json::Value::Members members = query.getMemberNames();
79 for (size_t i = 0; i < members.size(); i++)
80 {
81 DicomTag t = FromDcmtkBridge::ParseTag(members[i]);
82 result.SetValue(t, query[members[i]].asString());
83 }
84
85 return true;
86 }
87
88 static void DicomFindPatient(RestApi::PostCall& call)
89 {
90 DicomMap m;
91 DicomMap::SetupFindPatientTemplate(m);
92 if (!MergeQueryAndTemplate(m, call.GetPostBody()))
93 {
94 return;
95 }
96
97 DicomUserConnection connection;
98 ConnectToModalityUsingSymbolicName(connection, call.GetUriComponent("id", ""));
99
100 DicomFindAnswers answers;
101 connection.FindPatient(answers, m);
102
103 Json::Value result;
104 answers.ToJson(result);
105 call.GetOutput().AnswerJson(result);
106 }
107
108 static void DicomFindStudy(RestApi::PostCall& call)
109 {
110 DicomMap m;
111 DicomMap::SetupFindStudyTemplate(m);
112 if (!MergeQueryAndTemplate(m, call.GetPostBody()))
113 {
114 return;
115 }
116
117 if (m.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 &&
118 m.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2)
119 {
120 return;
121 }
122
123 DicomUserConnection connection;
124 ConnectToModalityUsingSymbolicName(connection, call.GetUriComponent("id", ""));
125
126 DicomFindAnswers answers;
127 connection.FindStudy(answers, m);
128
129 Json::Value result;
130 answers.ToJson(result);
131 call.GetOutput().AnswerJson(result);
132 }
133
134 static void DicomFindSeries(RestApi::PostCall& call)
135 {
136 DicomMap m;
137 DicomMap::SetupFindSeriesTemplate(m);
138 if (!MergeQueryAndTemplate(m, call.GetPostBody()))
139 {
140 return;
141 }
142
143 if ((m.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 &&
144 m.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2) ||
145 m.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString().size() <= 2)
146 {
147 return;
148 }
149
150 DicomUserConnection connection;
151 ConnectToModalityUsingSymbolicName(connection, call.GetUriComponent("id", ""));
152
153 DicomFindAnswers answers;
154 connection.FindSeries(answers, m);
155
156 Json::Value result;
157 answers.ToJson(result);
158 call.GetOutput().AnswerJson(result);
159 }
160
161 static void DicomFindInstance(RestApi::PostCall& call)
162 {
163 DicomMap m;
164 DicomMap::SetupFindInstanceTemplate(m);
165 if (!MergeQueryAndTemplate(m, call.GetPostBody()))
166 {
167 return;
168 }
169
170 if ((m.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 &&
171 m.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2) ||
172 m.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString().size() <= 2 ||
173 m.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).AsString().size() <= 2)
174 {
175 return;
176 }
177
178 DicomUserConnection connection;
179 ConnectToModalityUsingSymbolicName(connection, call.GetUriComponent("id", ""));
180
181 DicomFindAnswers answers;
182 connection.FindInstance(answers, m);
183
184 Json::Value result;
185 answers.ToJson(result);
186 call.GetOutput().AnswerJson(result);
187 }
188
189 static void DicomFind(RestApi::PostCall& call)
190 {
191 DicomMap m;
192 DicomMap::SetupFindPatientTemplate(m);
193 if (!MergeQueryAndTemplate(m, call.GetPostBody()))
194 {
195 return;
196 }
197
198 DicomUserConnection connection;
199 ConnectToModalityUsingSymbolicName(connection, call.GetUriComponent("id", ""));
200
201 DicomFindAnswers patients;
202 connection.FindPatient(patients, m);
203
204 // Loop over the found patients
205 Json::Value result = Json::arrayValue;
206 for (size_t i = 0; i < patients.GetSize(); i++)
207 {
208 Json::Value patient(Json::objectValue);
209 FromDcmtkBridge::ToJson(patient, patients.GetAnswer(i));
210
211 DicomMap::SetupFindStudyTemplate(m);
212 if (!MergeQueryAndTemplate(m, call.GetPostBody()))
213 {
214 return;
215 }
216 m.CopyTagIfExists(patients.GetAnswer(i), DICOM_TAG_PATIENT_ID);
217
218 DicomFindAnswers studies;
219 connection.FindStudy(studies, m);
220
221 patient["Studies"] = Json::arrayValue;
222
223 // Loop over the found studies
224 for (size_t j = 0; j < studies.GetSize(); j++)
225 {
226 Json::Value study(Json::objectValue);
227 FromDcmtkBridge::ToJson(study, studies.GetAnswer(j));
228
229 DicomMap::SetupFindSeriesTemplate(m);
230 if (!MergeQueryAndTemplate(m, call.GetPostBody()))
231 {
232 return;
233 }
234 m.CopyTagIfExists(studies.GetAnswer(j), DICOM_TAG_PATIENT_ID);
235 m.CopyTagIfExists(studies.GetAnswer(j), DICOM_TAG_STUDY_INSTANCE_UID);
236
237 DicomFindAnswers series;
238 connection.FindSeries(series, m);
239
240 // Loop over the found series
241 study["Series"] = Json::arrayValue;
242 for (size_t k = 0; k < series.GetSize(); k++)
243 {
244 Json::Value series2(Json::objectValue);
245 FromDcmtkBridge::ToJson(series2, series.GetAnswer(k));
246 study["Series"].append(series2);
247 }
248
249 patient["Studies"].append(study);
250 }
251
252 result.append(patient);
253 }
254
255 call.GetOutput().AnswerJson(result);
256 }
257
258
259 static bool GetInstancesToExport(std::list<std::string>& instances,
260 const std::string& remote,
261 RestApi::PostCall& call)
262 {
263 ServerContext& context = OrthancRestApi::GetContext(call);
264
265 std::string stripped = Toolbox::StripSpaces(call.GetPostBody());
266
267 Json::Value request;
268 if (Toolbox::IsSHA1(stripped))
269 {
270 // This is for compatibility with Orthanc <= 0.5.1.
271 request = stripped;
272 }
273 else if (!call.ParseJsonRequest(request))
274 {
275 // Bad JSON request
276 return false;
277 }
278
279 if (request.isString())
280 {
281 context.GetIndex().LogExportedResource(request.asString(), remote);
282 context.GetIndex().GetChildInstances(instances, request.asString());
283 }
284 else if (request.isArray())
285 {
286 for (Json::Value::ArrayIndex i = 0; i < request.size(); i++)
287 {
288 if (!request[i].isString())
289 {
290 return false;
291 }
292
293 std::string stripped = Toolbox::StripSpaces(request[i].asString());
294 if (!Toolbox::IsSHA1(stripped))
295 {
296 return false;
297 }
298
299 context.GetIndex().LogExportedResource(stripped, remote);
300
301 std::list<std::string> tmp;
302 context.GetIndex().GetChildInstances(tmp, stripped);
303
304 for (std::list<std::string>::const_iterator
305 it = tmp.begin(); it != tmp.end(); ++it)
306 {
307 instances.push_back(*it);
308 }
309 }
310 }
311 else
312 {
313 // Neither a string, nor a list of strings. Bad request.
314 return false;
315 }
316
317 return true;
318 }
319
320
321 static void DicomStore(RestApi::PostCall& call)
322 {
323 ServerContext& context = OrthancRestApi::GetContext(call);
324
325 std::string remote = call.GetUriComponent("id", "");
326
327 std::list<std::string> instances;
328 if (!GetInstancesToExport(instances, remote, call))
329 {
330 return;
331 }
332
333 DicomUserConnection connection;
334 ConnectToModalityUsingSymbolicName(connection, remote);
335
336 for (std::list<std::string>::const_iterator
337 it = instances.begin(); it != instances.end(); ++it)
338 {
339 LOG(INFO) << "Sending resource " << *it << " to modality \"" << remote << "\"";
340
341 std::string dicom;
342 context.ReadFile(dicom, *it, FileContentType_Dicom);
343 connection.Store(dicom);
344 }
345
346 call.GetOutput().AnswerBuffer("{}", "application/json");
347 }
348
349
350
351 // System information -------------------------------------------------------
352
353 static void ServeRoot(RestApi::GetCall& call)
354 {
355 call.GetOutput().Redirect("app/explorer.html");
356 }
357
358 static void GetSystemInformation(RestApi::GetCall& call)
359 {
360 Json::Value result = Json::objectValue;
361
362 result["Version"] = ORTHANC_VERSION;
363 result["Name"] = GetGlobalStringParameter("Name", "");
364
365 call.GetOutput().AnswerJson(result);
366 }
367
368 static void GetStatistics(RestApi::GetCall& call)
369 {
370 Json::Value result = Json::objectValue;
371 OrthancRestApi::GetIndex(call).ComputeStatistics(result);
372 call.GetOutput().AnswerJson(result);
373 }
374
375 static void GenerateUid(RestApi::GetCall& call)
376 {
377 std::string level = call.GetArgument("level", "");
378 if (level == "patient")
379 {
380 call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Patient), "text/plain");
381 }
382 else if (level == "study")
383 {
384 call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Study), "text/plain");
385 }
386 else if (level == "series")
387 {
388 call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Series), "text/plain");
389 }
390 else if (level == "instance")
391 {
392 call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Instance), "text/plain");
393 }
394 }
395
396 static void ExecuteScript(RestApi::PostCall& call)
397 {
398 std::string result;
399 ServerContext& context = OrthancRestApi::GetContext(call);
400 context.GetLuaContext().Execute(result, call.GetPostBody());
401 call.GetOutput().AnswerBuffer(result, "text/plain");
402 }
403
404 static void GetNowIsoString(RestApi::GetCall& call)
405 {
406 call.GetOutput().AnswerBuffer(Toolbox::GetNowIsoString(), "text/plain");
407 }
408
409
410
411
412
413
414 // List all the patients, studies, series or instances ----------------------
415
416 template <enum ResourceType resourceType>
417 static void ListResources(RestApi::GetCall& call)
418 {
419 Json::Value result;
420 OrthancRestApi::GetIndex(call).GetAllUuids(result, resourceType);
421 call.GetOutput().AnswerJson(result);
422 }
423
424 template <enum ResourceType resourceType>
425 static void GetSingleResource(RestApi::GetCall& call)
426 {
427 Json::Value result;
428 if (OrthancRestApi::GetIndex(call).LookupResource(result, call.GetUriComponent("id", ""), resourceType))
429 {
430 call.GetOutput().AnswerJson(result);
431 }
432 }
433
434 template <enum ResourceType resourceType>
435 static void DeleteSingleResource(RestApi::DeleteCall& call)
436 {
437 Json::Value result;
438 if (OrthancRestApi::GetIndex(call).DeleteResource(result, call.GetUriComponent("id", ""), resourceType))
439 {
440 call.GetOutput().AnswerJson(result);
441 }
442 }
443
444
445 // Download of ZIP files ----------------------------------------------------
446
447
448 static std::string GetDirectoryNameInArchive(const Json::Value& resource,
449 ResourceType resourceType)
450 {
451 switch (resourceType)
452 {
453 case ResourceType_Patient:
454 {
455 std::string p = resource["MainDicomTags"]["PatientID"].asString();
456 std::string n = resource["MainDicomTags"]["PatientName"].asString();
457 return p + " " + n;
458 }
459
460 case ResourceType_Study:
461 {
462 return resource["MainDicomTags"]["StudyDescription"].asString();
463 }
464
465 case ResourceType_Series:
466 {
467 std::string d = resource["MainDicomTags"]["SeriesDescription"].asString();
468 std::string m = resource["MainDicomTags"]["Modality"].asString();
469 return m + " " + d;
470 }
471
472 default:
473 throw OrthancException(ErrorCode_InternalError);
474 }
475 }
476
477 static bool CreateRootDirectoryInArchive(HierarchicalZipWriter& writer,
478 ServerContext& context,
479 const Json::Value& resource,
480 ResourceType resourceType)
481 {
482 if (resourceType == ResourceType_Patient)
483 {
484 return true;
485 }
486
487 ResourceType parentType = GetParentResourceType(resourceType);
488 Json::Value parent;
489
490 switch (resourceType)
491 {
492 case ResourceType_Study:
493 {
494 if (!context.GetIndex().LookupResource(parent, resource["ParentPatient"].asString(), parentType))
495 {
496 return false;
497 }
498
499 break;
500 }
501
502 case ResourceType_Series:
503 if (!context.GetIndex().LookupResource(parent, resource["ParentStudy"].asString(), parentType) ||
504 !CreateRootDirectoryInArchive(writer, context, parent, parentType))
505 {
506 return false;
507 }
508 break;
509
510 default:
511 throw OrthancException(ErrorCode_NotImplemented);
512 }
513
514 writer.OpenDirectory(GetDirectoryNameInArchive(parent, parentType).c_str());
515 return true;
516 }
517
518 static bool ArchiveInstance(HierarchicalZipWriter& writer,
519 ServerContext& context,
520 const std::string& instancePublicId,
521 const char* filename)
522 {
523 Json::Value instance;
524
525 if (!context.GetIndex().LookupResource(instance, instancePublicId, ResourceType_Instance))
526 {
527 return false;
528 }
529
530 writer.OpenFile(filename);
531
532 std::string dicom;
533 context.ReadFile(dicom, instancePublicId, FileContentType_Dicom);
534 writer.Write(dicom);
535
536 return true;
537 }
538
539 static bool ArchiveInternal(HierarchicalZipWriter& writer,
540 ServerContext& context,
541 const std::string& publicId,
542 ResourceType resourceType,
543 bool isFirstLevel)
544 {
545 Json::Value resource;
546 if (!context.GetIndex().LookupResource(resource, publicId, resourceType))
547 {
548 return false;
549 }
550
551 if (isFirstLevel &&
552 !CreateRootDirectoryInArchive(writer, context, resource, resourceType))
553 {
554 return false;
555 }
556
557 writer.OpenDirectory(GetDirectoryNameInArchive(resource, resourceType).c_str());
558
559 switch (resourceType)
560 {
561 case ResourceType_Patient:
562 for (Json::Value::ArrayIndex i = 0; i < resource["Studies"].size(); i++)
563 {
564 std::string studyId = resource["Studies"][i].asString();
565 if (!ArchiveInternal(writer, context, studyId, ResourceType_Study, false))
566 {
567 return false;
568 }
569 }
570 break;
571
572 case ResourceType_Study:
573 for (Json::Value::ArrayIndex i = 0; i < resource["Series"].size(); i++)
574 {
575 std::string seriesId = resource["Series"][i].asString();
576 if (!ArchiveInternal(writer, context, seriesId, ResourceType_Series, false))
577 {
578 return false;
579 }
580 }
581 break;
582
583 case ResourceType_Series:
584 {
585 // Create a filename prefix, depending on the modality
586 char format[16] = "%08d";
587
588 if (resource["MainDicomTags"].isMember("Modality"))
589 {
590 std::string modality = resource["MainDicomTags"]["Modality"].asString();
591
592 if (modality.size() == 1)
593 {
594 snprintf(format, sizeof(format) - 1, "%c%%07d", toupper(modality[0]));
595 }
596 else if (modality.size() >= 2)
597 {
598 snprintf(format, sizeof(format) - 1, "%c%c%%06d", toupper(modality[0]), toupper(modality[1]));
599 }
600 }
601
602 char filename[16];
603
604 for (Json::Value::ArrayIndex i = 0; i < resource["Instances"].size(); i++)
605 {
606 snprintf(filename, sizeof(filename) - 1, format, i);
607
608 std::string publicId = resource["Instances"][i].asString();
609
610 // This was the implementation up to Orthanc 0.7.0:
611 // std::string filename = instance["MainDicomTags"]["SOPInstanceUID"].asString() + ".dcm";
612
613 if (!ArchiveInstance(writer, context, publicId, filename))
614 {
615 return false;
616 }
617 }
618
619 break;
620 }
621
622 default:
623 throw OrthancException(ErrorCode_InternalError);
624 }
625
626 writer.CloseDirectory();
627 return true;
628 }
629
630 template <enum ResourceType resourceType>
631 static void GetArchive(RestApi::GetCall& call)
632 {
633 ServerContext& context = OrthancRestApi::GetContext(call);
634
635 std::string id = call.GetUriComponent("id", "");
636
637 /**
638 * Determine whether ZIP64 is required. Original ZIP format can
639 * store up to 2GB of data (some implementation supporting up to
640 * 4GB of data), and up to 65535 files.
641 * https://en.wikipedia.org/wiki/Zip_(file_format)#ZIP64
642 **/
643
644 uint64_t uncompressedSize;
645 uint64_t compressedSize;
646 unsigned int countStudies;
647 unsigned int countSeries;
648 unsigned int countInstances;
649 context.GetIndex().GetStatistics(compressedSize, uncompressedSize,
650 countStudies, countSeries, countInstances, id);
651 const bool isZip64 = (uncompressedSize >= 2 * GIGA_BYTES ||
652 countInstances >= 65535);
653
654 LOG(INFO) << "Creating a ZIP file with " << countInstances << " files of size "
655 << (uncompressedSize / MEGA_BYTES) << "MB using the "
656 << (isZip64 ? "ZIP64" : "ZIP32") << " file format";
657
658 // Create a RAII for the temporary file to manage the ZIP file
659 Toolbox::TemporaryFile tmp;
660
661 {
662 // Create a ZIP writer
663 HierarchicalZipWriter writer(tmp.GetPath().c_str());
664 writer.SetZip64(isZip64);
665
666 // Store the requested resource into the ZIP
667 if (!ArchiveInternal(writer, context, id, resourceType, true))
668 {
669 return;
670 }
671 }
672
673 // Prepare the sending of the ZIP file
674 FilesystemHttpSender sender(tmp.GetPath().c_str());
675 sender.SetContentType("application/zip");
676 sender.SetDownloadFilename(id + ".zip");
677
678 // Send the ZIP
679 call.GetOutput().AnswerFile(sender);
680
681 // The temporary file is automatically removed thanks to the RAII
682 }
683
684
685 // Changes API --------------------------------------------------------------
686
687 static void GetSinceAndLimit(int64_t& since,
688 unsigned int& limit,
689 bool& last,
690 const RestApi::GetCall& call)
691 {
692 static const unsigned int MAX_RESULTS = 100;
693
694 if (call.HasArgument("last"))
695 {
696 last = true;
697 return;
698 }
699
700 last = false;
701
702 try
703 {
704 since = boost::lexical_cast<int64_t>(call.GetArgument("since", "0"));
705 limit = boost::lexical_cast<unsigned int>(call.GetArgument("limit", "0"));
706 }
707 catch (boost::bad_lexical_cast)
708 {
709 return;
710 }
711
712 if (limit == 0 || limit > MAX_RESULTS)
713 {
714 limit = MAX_RESULTS;
715 }
716 }
717
718 static void GetChanges(RestApi::GetCall& call)
719 {
720 ServerContext& context = OrthancRestApi::GetContext(call);
721
722 //std::string filter = GetArgument(getArguments, "filter", "");
723 int64_t since;
724 unsigned int limit;
725 bool last;
726 GetSinceAndLimit(since, limit, last, call);
727
728 Json::Value result;
729 if ((!last && context.GetIndex().GetChanges(result, since, limit)) ||
730 ( last && context.GetIndex().GetLastChange(result)))
731 {
732 call.GetOutput().AnswerJson(result);
733 }
734 }
735
736
737 static void DeleteChanges(RestApi::DeleteCall& call)
738 {
739 OrthancRestApi::GetIndex(call).DeleteChanges();
740 call.GetOutput().AnswerBuffer("", "text/plain");
741 }
742
743
744 static void GetExports(RestApi::GetCall& call)
745 {
746 ServerContext& context = OrthancRestApi::GetContext(call);
747
748 int64_t since;
749 unsigned int limit;
750 bool last;
751 GetSinceAndLimit(since, limit, last, call);
752
753 Json::Value result;
754 if ((!last && context.GetIndex().GetExportedResources(result, since, limit)) ||
755 ( last && context.GetIndex().GetLastExportedResource(result)))
756 {
757 call.GetOutput().AnswerJson(result);
758 }
759 }
760
761
762 static void DeleteExports(RestApi::DeleteCall& call)
763 {
764 OrthancRestApi::GetIndex(call).DeleteExportedResources();
765 call.GetOutput().AnswerBuffer("", "text/plain");
766 }
767
768
769 // Get information about a single patient -----------------------------------
770
771 static void IsProtectedPatient(RestApi::GetCall& call)
772 {
773 std::string publicId = call.GetUriComponent("id", "");
774 bool isProtected = OrthancRestApi::GetIndex(call).IsProtectedPatient(publicId);
775 call.GetOutput().AnswerBuffer(isProtected ? "1" : "0", "text/plain");
776 }
777
778
779 static void SetPatientProtection(RestApi::PutCall& call)
780 {
781 ServerContext& context = OrthancRestApi::GetContext(call);
782
783 std::string publicId = call.GetUriComponent("id", "");
784 std::string s = Toolbox::StripSpaces(call.GetPutBody());
785
786 if (s == "0")
787 {
788 context.GetIndex().SetProtectedPatient(publicId, false);
789 call.GetOutput().AnswerBuffer("", "text/plain");
790 }
791 else if (s == "1")
792 {
793 context.GetIndex().SetProtectedPatient(publicId, true);
794 call.GetOutput().AnswerBuffer("", "text/plain");
795 }
796 else
797 {
798 // Bad request
799 }
800 }
801
802
803 // Get information about a single instance ----------------------------------
804
805 static void GetInstanceFile(RestApi::GetCall& call)
806 {
807 ServerContext& context = OrthancRestApi::GetContext(call);
808
809 std::string publicId = call.GetUriComponent("id", "");
810 context.AnswerDicomFile(call.GetOutput(), publicId, FileContentType_Dicom);
811 }
812
813
814 static void ExportInstanceFile(RestApi::PostCall& call)
815 {
816 ServerContext& context = OrthancRestApi::GetContext(call);
817
818 std::string publicId = call.GetUriComponent("id", "");
819
820 std::string dicom;
821 context.ReadFile(dicom, publicId, FileContentType_Dicom);
822
823 Toolbox::WriteFile(dicom, call.GetPostBody());
824
825 call.GetOutput().AnswerBuffer("{}", "application/json");
826 }
827
828
829 template <bool simplify>
830 static void GetInstanceTags(RestApi::GetCall& call)
831 {
832 ServerContext& context = OrthancRestApi::GetContext(call);
833
834 std::string publicId = call.GetUriComponent("id", "");
835
836 Json::Value full;
837 context.ReadJson(full, publicId);
838
839 if (simplify)
840 {
841 Json::Value simplified;
842 SimplifyTags(simplified, full);
843 call.GetOutput().AnswerJson(simplified);
844 }
845 else
846 {
847 call.GetOutput().AnswerJson(full);
848 }
849 }
850
851
852 static void ListFrames(RestApi::GetCall& call)
853 {
854 Json::Value instance;
855 if (OrthancRestApi::GetIndex(call).LookupResource(instance, call.GetUriComponent("id", ""), ResourceType_Instance))
856 {
857 unsigned int numberOfFrames = 1;
858
859 try
860 {
861 Json::Value tmp = instance["MainDicomTags"]["NumberOfFrames"];
862 numberOfFrames = boost::lexical_cast<unsigned int>(tmp.asString());
863 }
864 catch (...)
865 {
866 }
867
868 Json::Value result = Json::arrayValue;
869 for (unsigned int i = 0; i < numberOfFrames; i++)
870 {
871 result.append(i);
872 }
873
874 call.GetOutput().AnswerJson(result);
875 }
876 }
877
878
879 template <enum ImageExtractionMode mode>
880 static void GetImage(RestApi::GetCall& call)
881 {
882 ServerContext& context = OrthancRestApi::GetContext(call);
883
884 std::string frameId = call.GetUriComponent("frame", "0");
885
886 unsigned int frame;
887 try
888 {
889 frame = boost::lexical_cast<unsigned int>(frameId);
890 }
891 catch (boost::bad_lexical_cast)
892 {
893 return;
894 }
895
896 std::string publicId = call.GetUriComponent("id", "");
897 std::string dicomContent, png;
898 context.ReadFile(dicomContent, publicId, FileContentType_Dicom);
899
900 try
901 {
902 FromDcmtkBridge::ExtractPngImage(png, dicomContent, frame, mode);
903 call.GetOutput().AnswerBuffer(png, "image/png");
904 }
905 catch (OrthancException& e)
906 {
907 if (e.GetErrorCode() == ErrorCode_ParameterOutOfRange)
908 {
909 // The frame number is out of the range for this DICOM
910 // instance, the resource is not existent
911 }
912 else
913 {
914 std::string root = "";
915 for (size_t i = 1; i < call.GetFullUri().size(); i++)
916 {
917 root += "../";
918 }
919
920 call.GetOutput().Redirect(root + "app/images/unsupported.png");
921 }
922 }
923 }
924
925
926 // Upload of DICOM files through HTTP ---------------------------------------
927
928 static void UploadDicomFile(RestApi::PostCall& call)
929 {
930 ServerContext& context = OrthancRestApi::GetContext(call);
931
932 const std::string& postData = call.GetPostBody();
933 if (postData.size() == 0)
934 {
935 return;
936 }
937
938 LOG(INFO) << "Receiving a DICOM file of " << postData.size() << " bytes through HTTP";
939
940 std::string publicId;
941 StoreStatus status = context.Store(publicId, postData);
942 Json::Value result = Json::objectValue;
943
944 if (status != StoreStatus_Failure)
945 {
946 result["ID"] = publicId;
947 result["Path"] = GetBasePath(ResourceType_Instance, publicId);
948 }
949
950 result["Status"] = EnumerationToString(status);
951 call.GetOutput().AnswerJson(result);
952 }
953
954
955
956 // DICOM bridge -------------------------------------------------------------
957
958 static bool IsExistingModality(const OrthancRestApi::SetOfStrings& modalities,
959 const std::string& id)
960 {
961 return modalities.find(id) != modalities.end();
962 }
963
964 static void ListModalities(RestApi::GetCall& call)
965 {
966 OrthancRestApi::SetOfStrings modalities;
967 GetListOfDicomModalities(modalities);
968
969 Json::Value result = Json::arrayValue;
970 for (OrthancRestApi::SetOfStrings::const_iterator
971 it = modalities.begin(); it != modalities.end(); ++it)
972 {
973 result.append(*it);
974 }
975
976 call.GetOutput().AnswerJson(result);
977 }
978
979
980 static void ListModalityOperations(RestApi::GetCall& call)
981 {
982 OrthancRestApi::SetOfStrings modalities;
983 GetListOfDicomModalities(modalities);
984
985 std::string id = call.GetUriComponent("id", "");
986 if (IsExistingModality(modalities, id))
987 {
988 Json::Value result = Json::arrayValue;
989 result.append("find-patient");
990 result.append("find-study");
991 result.append("find-series");
992 result.append("find-instance");
993 result.append("find");
994 result.append("store");
995 call.GetOutput().AnswerJson(result);
996 }
997 }
998
999
1000
1001 // Raw access to the DICOM tags of an instance ------------------------------
1002
1003 static void GetRawContent(RestApi::GetCall& call)
1004 {
1005 boost::mutex::scoped_lock lock(cacheMutex_);
1006
1007 ServerContext& context = OrthancRestApi::GetContext(call);
1008
1009 std::string id = call.GetUriComponent("id", "");
1010 ParsedDicomFile& dicom = context.GetDicomFile(id);
1011 dicom.SendPathValue(call.GetOutput(), call.GetTrailingUri());
1012 }
1013
1014
1015
1016 // Modification of DICOM instances ------------------------------------------
1017
1018 namespace
1019 {
1020 typedef std::set<DicomTag> Removals;
1021 typedef std::map<DicomTag, std::string> Replacements;
1022 typedef std::map< std::pair<DicomRootLevel, std::string>, std::string> UidMap;
1023 }
1024
1025 static void ReplaceInstanceInternal(ParsedDicomFile& toModify,
1026 const Removals& removals,
1027 const Replacements& replacements,
1028 DicomReplaceMode mode,
1029 bool removePrivateTags)
1030 {
1031 if (removePrivateTags)
1032 {
1033 toModify.RemovePrivateTags();
1034 }
1035
1036 for (Removals::const_iterator it = removals.begin();
1037 it != removals.end(); ++it)
1038 {
1039 toModify.Remove(*it);
1040 }
1041
1042 for (Replacements::const_iterator it = replacements.begin();
1043 it != replacements.end(); ++it)
1044 {
1045 toModify.Replace(it->first, it->second, mode);
1046 }
1047
1048 // A new SOP instance UID is automatically generated
1049 std::string instanceUid = FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Instance);
1050 toModify.Replace(DICOM_TAG_SOP_INSTANCE_UID, instanceUid, DicomReplaceMode_InsertIfAbsent);
1051 }
1052
1053
1054 static void ParseRemovals(Removals& target,
1055 const Json::Value& removals)
1056 {
1057 if (!removals.isArray())
1058 {
1059 throw OrthancException(ErrorCode_BadRequest);
1060 }
1061
1062 for (Json::Value::ArrayIndex i = 0; i < removals.size(); i++)
1063 {
1064 std::string name = removals[i].asString();
1065 DicomTag tag = FromDcmtkBridge::ParseTag(name);
1066 target.insert(tag);
1067
1068 VLOG(1) << "Removal: " << name << " " << tag << std::endl;
1069 }
1070 }
1071
1072
1073 static void ParseReplacements(Replacements& target,
1074 const Json::Value& replacements)
1075 {
1076 if (!replacements.isObject())
1077 {
1078 throw OrthancException(ErrorCode_BadRequest);
1079 }
1080
1081 Json::Value::Members members = replacements.getMemberNames();
1082 for (size_t i = 0; i < members.size(); i++)
1083 {
1084 const std::string& name = members[i];
1085 std::string value = replacements[name].asString();
1086
1087 DicomTag tag = FromDcmtkBridge::ParseTag(name);
1088 target[tag] = value;
1089
1090 VLOG(1) << "Replacement: " << name << " " << tag << " == " << value << std::endl;
1091 }
1092 }
1093
1094
1095 static std::string GeneratePatientName(ServerContext& context)
1096 {
1097 uint64_t seq = context.GetIndex().IncrementGlobalSequence(GlobalProperty_AnonymizationSequence);
1098 return "Anonymized" + boost::lexical_cast<std::string>(seq);
1099 }
1100
1101
1102 static void SetupAnonymization(Removals& removals,
1103 Replacements& replacements)
1104 {
1105 // This is Table E.1-1 from PS 3.15-2008 - DICOM Part 15: Security and System Management Profiles
1106 removals.insert(DicomTag(0x0008, 0x0014)); // Instance Creator UID
1107 //removals.insert(DicomTag(0x0008, 0x0018)); // SOP Instance UID => set by ReplaceInstanceInternal()
1108 removals.insert(DicomTag(0x0008, 0x0050)); // Accession Number
1109 removals.insert(DicomTag(0x0008, 0x0080)); // Institution Name
1110 removals.insert(DicomTag(0x0008, 0x0081)); // Institution Address
1111 removals.insert(DicomTag(0x0008, 0x0090)); // Referring Physician's Name
1112 removals.insert(DicomTag(0x0008, 0x0092)); // Referring Physician's Address
1113 removals.insert(DicomTag(0x0008, 0x0094)); // Referring Physician's Telephone Numbers
1114 removals.insert(DicomTag(0x0008, 0x1010)); // Station Name
1115 removals.insert(DicomTag(0x0008, 0x1030)); // Study Description
1116 removals.insert(DicomTag(0x0008, 0x103e)); // Series Description
1117 removals.insert(DicomTag(0x0008, 0x1040)); // Institutional Department Name
1118 removals.insert(DicomTag(0x0008, 0x1048)); // Physician(s) of Record
1119 removals.insert(DicomTag(0x0008, 0x1050)); // Performing Physicians' Name
1120 removals.insert(DicomTag(0x0008, 0x1060)); // Name of Physician(s) Reading Study
1121 removals.insert(DicomTag(0x0008, 0x1070)); // Operators' Name
1122 removals.insert(DicomTag(0x0008, 0x1080)); // Admitting Diagnoses Description
1123 removals.insert(DicomTag(0x0008, 0x1155)); // Referenced SOP Instance UID
1124 removals.insert(DicomTag(0x0008, 0x2111)); // Derivation Description
1125 removals.insert(DicomTag(0x0010, 0x0010)); // Patient's Name
1126 //removals.insert(DicomTag(0x0010, 0x0020)); // Patient ID => cf. below (*)
1127 removals.insert(DicomTag(0x0010, 0x0030)); // Patient's Birth Date
1128 removals.insert(DicomTag(0x0010, 0x0032)); // Patient's Birth Time
1129 removals.insert(DicomTag(0x0010, 0x0040)); // Patient's Sex
1130 removals.insert(DicomTag(0x0010, 0x1000)); // Other Patient Ids
1131 removals.insert(DicomTag(0x0010, 0x1001)); // Other Patient Names
1132 removals.insert(DicomTag(0x0010, 0x1010)); // Patient's Age
1133 removals.insert(DicomTag(0x0010, 0x1020)); // Patient's Size
1134 removals.insert(DicomTag(0x0010, 0x1030)); // Patient's Weight
1135 removals.insert(DicomTag(0x0010, 0x1090)); // Medical Record Locator
1136 removals.insert(DicomTag(0x0010, 0x2160)); // Ethnic Group
1137 removals.insert(DicomTag(0x0010, 0x2180)); // Occupation
1138 removals.insert(DicomTag(0x0010, 0x21b0)); // Additional Patient's History
1139 removals.insert(DicomTag(0x0010, 0x4000)); // Patient Comments
1140 removals.insert(DicomTag(0x0018, 0x1000)); // Device Serial Number
1141 removals.insert(DicomTag(0x0018, 0x1030)); // Protocol Name
1142 //removals.insert(DicomTag(0x0020, 0x000d)); // Study Instance UID => cf. below (*)
1143 //removals.insert(DicomTag(0x0020, 0x000e)); // Series Instance UID => cf. below (*)
1144 removals.insert(DicomTag(0x0020, 0x0010)); // Study ID
1145 removals.insert(DicomTag(0x0020, 0x0052)); // Frame of Reference UID
1146 removals.insert(DicomTag(0x0020, 0x0200)); // Synchronization Frame of Reference UID
1147 removals.insert(DicomTag(0x0020, 0x4000)); // Image Comments
1148 removals.insert(DicomTag(0x0040, 0x0275)); // Request Attributes Sequence
1149 removals.insert(DicomTag(0x0040, 0xa124)); // UID
1150 removals.insert(DicomTag(0x0040, 0xa730)); // Content Sequence
1151 removals.insert(DicomTag(0x0088, 0x0140)); // Storage Media File-set UID
1152 removals.insert(DicomTag(0x3006, 0x0024)); // Referenced Frame of Reference UID
1153 removals.insert(DicomTag(0x3006, 0x00c2)); // Related Frame of Reference UID
1154
1155 /**
1156 * (*) Patient ID, Study Instance UID and Series Instance UID
1157 * are modified by "AnonymizeInstance()" if anonymizing a single
1158 * instance, or by "RetrieveMappedUid()" if anonymizing a
1159 * patient/study/series.
1160 **/
1161
1162
1163 // Some more removals (from the experience of DICOM files at the CHU of Liege)
1164 removals.insert(DicomTag(0x0010, 0x1040)); // Patient's Address
1165 removals.insert(DicomTag(0x0032, 0x1032)); // Requesting Physician
1166 removals.insert(DicomTag(0x0010, 0x2154)); // PatientTelephoneNumbers
1167 removals.insert(DicomTag(0x0010, 0x2000)); // Medical Alerts
1168
1169 // Set the DeidentificationMethod tag
1170 replacements.insert(std::make_pair(DicomTag(0x0012, 0x0063), "Orthanc " ORTHANC_VERSION " - PS 3.15-2008 Table E.1-1"));
1171
1172 // Set the PatientIdentityRemoved tag
1173 replacements.insert(std::make_pair(DicomTag(0x0012, 0x0062), "YES"));
1174 }
1175
1176
1177 static bool ParseModifyRequest(Removals& removals,
1178 Replacements& replacements,
1179 bool& removePrivateTags,
1180 const RestApi::PostCall& call)
1181 {
1182 removePrivateTags = false;
1183 Json::Value request;
1184 if (call.ParseJsonRequest(request) &&
1185 request.isObject())
1186 {
1187 Json::Value removalsPart = Json::arrayValue;
1188 Json::Value replacementsPart = Json::objectValue;
1189
1190 if (request.isMember("Remove"))
1191 {
1192 removalsPart = request["Remove"];
1193 }
1194
1195 if (request.isMember("Replace"))
1196 {
1197 replacementsPart = request["Replace"];
1198 }
1199
1200 if (request.isMember("RemovePrivateTags"))
1201 {
1202 removePrivateTags = true;
1203 }
1204
1205 ParseRemovals(removals, removalsPart);
1206 ParseReplacements(replacements, replacementsPart);
1207
1208 return true;
1209 }
1210 else
1211 {
1212 return false;
1213 }
1214 }
1215
1216
1217 static bool ParseAnonymizationRequest(Removals& removals,
1218 Replacements& replacements,
1219 bool& removePrivateTags,
1220 bool& keepPatientId,
1221 RestApi::PostCall& call)
1222 {
1223 ServerContext& context = OrthancRestApi::GetContext(call);
1224
1225 removePrivateTags = true;
1226 keepPatientId = false;
1227
1228 Json::Value request;
1229 if (call.ParseJsonRequest(request) &&
1230 request.isObject())
1231 {
1232 Json::Value keepPart = Json::arrayValue;
1233 Json::Value removalsPart = Json::arrayValue;
1234 Json::Value replacementsPart = Json::objectValue;
1235
1236 if (request.isMember("Keep"))
1237 {
1238 keepPart = request["Keep"];
1239 }
1240
1241 if (request.isMember("KeepPrivateTags"))
1242 {
1243 removePrivateTags = false;
1244 }
1245
1246 if (request.isMember("Replace"))
1247 {
1248 replacementsPart = request["Replace"];
1249 }
1250
1251 Removals toKeep;
1252 ParseRemovals(toKeep, keepPart);
1253
1254 SetupAnonymization(removals, replacements);
1255
1256 for (Removals::iterator it = toKeep.begin(); it != toKeep.end(); ++it)
1257 {
1258 if (*it == DICOM_TAG_PATIENT_ID)
1259 {
1260 keepPatientId = true;
1261 }
1262
1263 removals.erase(*it);
1264 }
1265
1266 Removals additionalRemovals;
1267 ParseRemovals(additionalRemovals, removalsPart);
1268
1269 for (Removals::iterator it = additionalRemovals.begin();
1270 it != additionalRemovals.end(); ++it)
1271 {
1272 removals.insert(*it);
1273 }
1274
1275 ParseReplacements(replacements, replacementsPart);
1276
1277 // Generate random Patient's Name if none is specified
1278 if (toKeep.find(DICOM_TAG_PATIENT_NAME) == toKeep.end() &&
1279 replacements.find(DICOM_TAG_PATIENT_NAME) == replacements.end())
1280 {
1281 replacements.insert(std::make_pair(DICOM_TAG_PATIENT_NAME, GeneratePatientName(context)));
1282 }
1283
1284 return true;
1285 }
1286 else
1287 {
1288 return false;
1289 }
1290 }
1291
1292
1293 static void AnonymizeOrModifyInstance(Removals& removals,
1294 Replacements& replacements,
1295 bool removePrivateTags,
1296 RestApi::PostCall& call)
1297 {
1298 boost::mutex::scoped_lock lock(cacheMutex_);
1299 ServerContext& context = OrthancRestApi::GetContext(call);
1300
1301 std::string id = call.GetUriComponent("id", "");
1302 ParsedDicomFile& dicom = context.GetDicomFile(id);
1303
1304 std::auto_ptr<ParsedDicomFile> modified(dicom.Clone());
1305 ReplaceInstanceInternal(*modified, removals, replacements, DicomReplaceMode_InsertIfAbsent, removePrivateTags);
1306 modified->Answer(call.GetOutput());
1307 }
1308
1309
1310 static bool RetrieveMappedUid(ParsedDicomFile& dicom,
1311 DicomRootLevel level,
1312 Replacements& replacements,
1313 UidMap& uidMap)
1314 {
1315 std::auto_ptr<DicomTag> tag;
1316
1317 switch (level)
1318 {
1319 case DicomRootLevel_Series:
1320 tag.reset(new DicomTag(DICOM_TAG_SERIES_INSTANCE_UID));
1321 break;
1322
1323 case DicomRootLevel_Study:
1324 tag.reset(new DicomTag(DICOM_TAG_STUDY_INSTANCE_UID));
1325 break;
1326
1327 case DicomRootLevel_Patient:
1328 tag.reset(new DicomTag(DICOM_TAG_PATIENT_ID));
1329 break;
1330
1331 default:
1332 throw OrthancException(ErrorCode_InternalError);
1333 }
1334
1335 std::string original;
1336 if (!dicom.GetTagValue(original, *tag))
1337 {
1338 throw OrthancException(ErrorCode_InternalError);
1339 }
1340
1341 std::string mapped;
1342 bool isNew;
1343
1344 UidMap::const_iterator previous = uidMap.find(std::make_pair(level, original));
1345 if (previous == uidMap.end())
1346 {
1347 mapped = FromDcmtkBridge::GenerateUniqueIdentifier(level);
1348 uidMap.insert(std::make_pair(std::make_pair(level, original), mapped));
1349 isNew = true;
1350 }
1351 else
1352 {
1353 mapped = previous->second;
1354 isNew = false;
1355 }
1356
1357 replacements[*tag] = mapped;
1358 return isNew;
1359 }
1360
1361
1362 static void AnonymizeOrModifyResource(Removals& removals,
1363 Replacements& replacements,
1364 bool removePrivateTags,
1365 bool keepPatientId,
1366 MetadataType metadataType,
1367 ChangeType changeType,
1368 ResourceType resourceType,
1369 RestApi::PostCall& call)
1370 {
1371 typedef std::list<std::string> Instances;
1372
1373 bool isFirst = true;
1374 Json::Value result(Json::objectValue);
1375
1376 boost::mutex::scoped_lock lock(cacheMutex_);
1377 ServerContext& context = OrthancRestApi::GetContext(call);
1378
1379 Instances instances;
1380 std::string id = call.GetUriComponent("id", "");
1381 context.GetIndex().GetChildInstances(instances, id);
1382
1383 if (instances.empty())
1384 {
1385 return;
1386 }
1387
1388 /**
1389 * Loop over all the instances of the resource.
1390 **/
1391
1392 UidMap uidMap;
1393 for (Instances::const_iterator it = instances.begin();
1394 it != instances.end(); ++it)
1395 {
1396 LOG(INFO) << "Modifying instance " << *it;
1397 ParsedDicomFile& original = context.GetDicomFile(*it);
1398
1399 DicomInstanceHasher originalHasher = original.GetHasher();
1400
1401 if (isFirst && keepPatientId)
1402 {
1403 std::string patientId = originalHasher.GetPatientId();
1404 uidMap[std::make_pair(DicomRootLevel_Patient, patientId)] = patientId;
1405 }
1406
1407 bool isNewSeries = RetrieveMappedUid(original, DicomRootLevel_Series, replacements, uidMap);
1408 bool isNewStudy = RetrieveMappedUid(original, DicomRootLevel_Study, replacements, uidMap);
1409 bool isNewPatient = RetrieveMappedUid(original, DicomRootLevel_Patient, replacements, uidMap);
1410
1411
1412 /**
1413 * Compute the resulting DICOM instance and store it into the Orthanc store.
1414 **/
1415
1416 std::auto_ptr<ParsedDicomFile> modified(original.Clone());
1417 ReplaceInstanceInternal(*modified, removals, replacements, DicomReplaceMode_InsertIfAbsent, removePrivateTags);
1418
1419 std::string modifiedInstance;
1420 if (context.Store(modifiedInstance, modified->GetDicom()) != StoreStatus_Success)
1421 {
1422 LOG(ERROR) << "Error while storing a modified instance " << *it;
1423 return;
1424 }
1425
1426
1427 /**
1428 * Record metadata information (AnonymizedFrom/ModifiedFrom).
1429 **/
1430
1431 DicomInstanceHasher modifiedHasher = modified->GetHasher();
1432
1433 if (isNewSeries)
1434 {
1435 context.GetIndex().SetMetadata(modifiedHasher.HashSeries(),
1436 metadataType, originalHasher.HashSeries());
1437 }
1438
1439 if (isNewStudy)
1440 {
1441 context.GetIndex().SetMetadata(modifiedHasher.HashStudy(),
1442 metadataType, originalHasher.HashStudy());
1443 }
1444
1445 if (isNewPatient)
1446 {
1447 context.GetIndex().SetMetadata(modifiedHasher.HashPatient(),
1448 metadataType, originalHasher.HashPatient());
1449 }
1450
1451 assert(*it == originalHasher.HashInstance());
1452 assert(modifiedInstance == modifiedHasher.HashInstance());
1453 context.GetIndex().SetMetadata(modifiedInstance, metadataType, *it);
1454
1455
1456 /**
1457 * Compute the JSON object that is returned by the REST call.
1458 **/
1459
1460 if (isFirst)
1461 {
1462 std::string newId;
1463
1464 switch (resourceType)
1465 {
1466 case ResourceType_Series:
1467 newId = modifiedHasher.HashSeries();
1468 break;
1469
1470 case ResourceType_Study:
1471 newId = modifiedHasher.HashStudy();
1472 break;
1473
1474 case ResourceType_Patient:
1475 newId = modifiedHasher.HashPatient();
1476 break;
1477
1478 default:
1479 throw OrthancException(ErrorCode_InternalError);
1480 }
1481
1482 result["Type"] = EnumerationToString(resourceType);
1483 result["ID"] = newId;
1484 result["Path"] = GetBasePath(resourceType, newId);
1485 result["PatientID"] = modifiedHasher.HashPatient();
1486 isFirst = false;
1487 }
1488 }
1489
1490 call.GetOutput().AnswerJson(result);
1491 }
1492
1493
1494
1495 static void ModifyInstance(RestApi::PostCall& call)
1496 {
1497 Removals removals;
1498 Replacements replacements;
1499 bool removePrivateTags;
1500
1501 if (ParseModifyRequest(removals, replacements, removePrivateTags, call))
1502 {
1503 AnonymizeOrModifyInstance(removals, replacements, removePrivateTags, call);
1504 }
1505 }
1506
1507
1508 static void AnonymizeInstance(RestApi::PostCall& call)
1509 {
1510 Removals removals;
1511 Replacements replacements;
1512 bool removePrivateTags, keepPatientId;
1513
1514 if (ParseAnonymizationRequest(removals, replacements, removePrivateTags, keepPatientId, call))
1515 {
1516 // TODO Handle "keepPatientId"
1517
1518 // Generate random patient ID if not specified
1519 if (replacements.find(DICOM_TAG_PATIENT_ID) == replacements.end())
1520 {
1521 replacements.insert(std::make_pair(DICOM_TAG_PATIENT_ID,
1522 FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Patient)));
1523 }
1524
1525 // Generate random study UID if not specified
1526 if (replacements.find(DICOM_TAG_STUDY_INSTANCE_UID) == replacements.end())
1527 {
1528 replacements.insert(std::make_pair(DICOM_TAG_STUDY_INSTANCE_UID,
1529 FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Study)));
1530 }
1531
1532 // Generate random series UID if not specified
1533 if (replacements.find(DICOM_TAG_SERIES_INSTANCE_UID) == replacements.end())
1534 {
1535 replacements.insert(std::make_pair(DICOM_TAG_SERIES_INSTANCE_UID,
1536 FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Series)));
1537 }
1538
1539 AnonymizeOrModifyInstance(removals, replacements, removePrivateTags, call);
1540 }
1541 }
1542
1543
1544 static void ModifySeriesInplace(RestApi::PostCall& call)
1545 {
1546 Removals removals;
1547 Replacements replacements;
1548 bool removePrivateTags;
1549
1550 if (ParseModifyRequest(removals, replacements, removePrivateTags, call))
1551 {
1552 AnonymizeOrModifyResource(removals, replacements, removePrivateTags, true /*keepPatientId*/,
1553 MetadataType_ModifiedFrom, ChangeType_ModifiedSeries,
1554 ResourceType_Series, call);
1555 }
1556 }
1557
1558
1559 static void AnonymizeSeriesInplace(RestApi::PostCall& call)
1560 {
1561 Removals removals;
1562 Replacements replacements;
1563 bool removePrivateTags, keepPatientId;
1564
1565 if (ParseAnonymizationRequest(removals, replacements, removePrivateTags, keepPatientId, call))
1566 {
1567 AnonymizeOrModifyResource(removals, replacements, removePrivateTags, keepPatientId,
1568 MetadataType_AnonymizedFrom, ChangeType_AnonymizedSeries,
1569 ResourceType_Series, call);
1570 }
1571 }
1572
1573
1574 static void ModifyStudyInplace(RestApi::PostCall& call)
1575 {
1576 Removals removals;
1577 Replacements replacements;
1578 bool removePrivateTags;
1579
1580 if (ParseModifyRequest(removals, replacements, removePrivateTags, call))
1581 {
1582 AnonymizeOrModifyResource(removals, replacements, removePrivateTags, true /*keepPatientId*/,
1583 MetadataType_ModifiedFrom, ChangeType_ModifiedStudy,
1584 ResourceType_Study, call);
1585 }
1586 }
1587
1588
1589 static void AnonymizeStudyInplace(RestApi::PostCall& call)
1590 {
1591 Removals removals;
1592 Replacements replacements;
1593 bool removePrivateTags, keepPatientId;
1594
1595 if (ParseAnonymizationRequest(removals, replacements, removePrivateTags, keepPatientId, call))
1596 {
1597 AnonymizeOrModifyResource(removals, replacements, removePrivateTags, keepPatientId,
1598 MetadataType_AnonymizedFrom, ChangeType_AnonymizedStudy,
1599 ResourceType_Study, call);
1600 }
1601 }
1602
1603
1604 /*static void ModifyPatientInplace(RestApi::PostCall& call)
1605 {
1606 Removals removals;
1607 Replacements replacements;
1608 bool removePrivateTags;
1609
1610 if (ParseModifyRequest(removals, replacements, removePrivateTags, call))
1611 {
1612 AnonymizeOrModifyResource(false, removals, replacements, removePrivateTags,
1613 MetadataType_ModifiedFrom, ChangeType_ModifiedPatient,
1614 ResourceType_Patient, call);
1615 }
1616 }*/
1617
1618
1619 static void AnonymizePatientInplace(RestApi::PostCall& call)
1620 {
1621 Removals removals;
1622 Replacements replacements;
1623 bool removePrivateTags, keepPatientId;
1624
1625 if (ParseAnonymizationRequest(removals, replacements, removePrivateTags, keepPatientId, call))
1626 {
1627 AnonymizeOrModifyResource(removals, replacements, removePrivateTags, keepPatientId,
1628 MetadataType_AnonymizedFrom, ChangeType_AnonymizedPatient,
1629 ResourceType_Patient, call);
1630 }
1631 }
1632
1633
1634 // Handling of metadata -----------------------------------------------------
1635
1636 static void CheckValidResourceType(RestApi::Call& call)
1637 {
1638 std::string resourceType = call.GetUriComponent("resourceType", "");
1639 StringToResourceType(resourceType.c_str());
1640 }
1641
1642
1643 static void ListMetadata(RestApi::GetCall& call)
1644 {
1645 CheckValidResourceType(call);
1646
1647 std::string publicId = call.GetUriComponent("id", "");
1648 std::list<MetadataType> metadata;
1649
1650 OrthancRestApi::GetIndex(call).ListAvailableMetadata(metadata, publicId);
1651 Json::Value result = Json::arrayValue;
1652
1653 for (std::list<MetadataType>::const_iterator
1654 it = metadata.begin(); it != metadata.end(); ++it)
1655 {
1656 result.append(EnumerationToString(*it));
1657 }
1658
1659 call.GetOutput().AnswerJson(result);
1660 }
1661
1662
1663 static void GetMetadata(RestApi::GetCall& call)
1664 {
1665 CheckValidResourceType(call);
1666
1667 std::string publicId = call.GetUriComponent("id", "");
1668 std::string name = call.GetUriComponent("name", "");
1669 MetadataType metadata = StringToMetadata(name);
1670
1671 std::string value;
1672 if (OrthancRestApi::GetIndex(call).LookupMetadata(value, publicId, metadata))
1673 {
1674 call.GetOutput().AnswerBuffer(value, "text/plain");
1675 }
1676 }
1677
1678
1679 static void DeleteMetadata(RestApi::DeleteCall& call)
1680 {
1681 CheckValidResourceType(call);
1682
1683 std::string publicId = call.GetUriComponent("id", "");
1684 std::string name = call.GetUriComponent("name", "");
1685 MetadataType metadata = StringToMetadata(name);
1686
1687 if (metadata >= MetadataType_StartUser &&
1688 metadata <= MetadataType_EndUser)
1689 {
1690 // It is forbidden to modify internal metadata
1691 OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata);
1692 call.GetOutput().AnswerBuffer("", "text/plain");
1693 }
1694 }
1695
1696
1697 static void SetMetadata(RestApi::PutCall& call)
1698 {
1699 CheckValidResourceType(call);
1700
1701 std::string publicId = call.GetUriComponent("id", "");
1702 std::string name = call.GetUriComponent("name", "");
1703 MetadataType metadata = StringToMetadata(name);
1704 std::string value = call.GetPutBody();
1705
1706 if (metadata >= MetadataType_StartUser &&
1707 metadata <= MetadataType_EndUser)
1708 {
1709 // It is forbidden to modify internal metadata
1710 OrthancRestApi::GetIndex(call).SetMetadata(publicId, metadata, value);
1711 call.GetOutput().AnswerBuffer("", "text/plain");
1712 }
1713 }
1714
1715
1716 static void GetResourceStatistics(RestApi::GetCall& call)
1717 {
1718 std::string publicId = call.GetUriComponent("id", "");
1719 Json::Value result;
1720 OrthancRestApi::GetIndex(call).GetStatistics(result, publicId);
1721 call.GetOutput().AnswerJson(result);
1722 }
1723
1724
1725
1726 // Orthanc Peers ------------------------------------------------------------
1727
1728 static bool IsExistingPeer(const OrthancRestApi::SetOfStrings& peers,
1729 const std::string& id)
1730 {
1731 return peers.find(id) != peers.end();
1732 }
1733
1734 static void ListPeers(RestApi::GetCall& call)
1735 {
1736 OrthancRestApi::SetOfStrings peers;
1737 GetListOfOrthancPeers(peers);
1738
1739 Json::Value result = Json::arrayValue;
1740 for (OrthancRestApi::SetOfStrings::const_iterator
1741 it = peers.begin(); it != peers.end(); ++it)
1742 {
1743 result.append(*it);
1744 }
1745
1746 call.GetOutput().AnswerJson(result);
1747 }
1748
1749 static void ListPeerOperations(RestApi::GetCall& call)
1750 {
1751 OrthancRestApi::SetOfStrings peers;
1752 GetListOfOrthancPeers(peers);
1753
1754 std::string id = call.GetUriComponent("id", "");
1755 if (IsExistingPeer(peers, id))
1756 {
1757 Json::Value result = Json::arrayValue;
1758 result.append("store");
1759 call.GetOutput().AnswerJson(result);
1760 }
1761 }
1762
1763 static void PeerStore(RestApi::PostCall& call)
1764 {
1765 ServerContext& context = OrthancRestApi::GetContext(call);
1766
1767 std::string remote = call.GetUriComponent("id", "");
1768
1769 std::list<std::string> instances;
1770 if (!GetInstancesToExport(instances, remote, call))
1771 {
1772 return;
1773 }
1774
1775 std::string url, username, password;
1776 GetOrthancPeer(remote, url, username, password);
1777
1778 // Configure the HTTP client
1779 HttpClient client;
1780 if (username.size() != 0 && password.size() != 0)
1781 {
1782 client.SetCredentials(username.c_str(), password.c_str());
1783 }
1784
1785 client.SetUrl(url + "instances");
1786 client.SetMethod(HttpMethod_Post);
1787
1788 // Loop over the instances that are to be sent
1789 for (std::list<std::string>::const_iterator
1790 it = instances.begin(); it != instances.end(); ++it)
1791 {
1792 LOG(INFO) << "Sending resource " << *it << " to peer \"" << remote << "\"";
1793
1794 context.ReadFile(client.AccessPostData(), *it, FileContentType_Dicom);
1795
1796 std::string answer;
1797 if (!client.Apply(answer))
1798 {
1799 LOG(ERROR) << "Unable to send resource " << *it << " to peer \"" << remote << "\"";
1800 return;
1801 }
1802 }
1803
1804 call.GetOutput().AnswerBuffer("{}", "application/json");
1805 }
1806
1807
1808
1809
1810
1811
1812 // Handling of attached files -----------------------------------------------
1813
1814 static void ListAttachments(RestApi::GetCall& call)
1815 {
1816 std::string resourceType = call.GetUriComponent("resourceType", "");
1817 std::string publicId = call.GetUriComponent("id", "");
1818 std::list<FileContentType> attachments;
1819 OrthancRestApi::GetIndex(call).ListAvailableAttachments(attachments, publicId, StringToResourceType(resourceType.c_str()));
1820
1821 Json::Value result = Json::arrayValue;
1822
1823 for (std::list<FileContentType>::const_iterator
1824 it = attachments.begin(); it != attachments.end(); ++it)
1825 {
1826 result.append(EnumerationToString(*it));
1827 }
1828
1829 call.GetOutput().AnswerJson(result);
1830 }
1831
1832
1833 static bool GetAttachmentInfo(FileInfo& info, RestApi::Call& call)
1834 {
1835 CheckValidResourceType(call);
1836
1837 std::string publicId = call.GetUriComponent("id", "");
1838 std::string name = call.GetUriComponent("name", "");
1839 FileContentType contentType = StringToContentType(name);
1840
1841 return OrthancRestApi::GetIndex(call).LookupAttachment(info, publicId, contentType);
1842 }
1843
1844
1845 static void GetAttachmentOperations(RestApi::GetCall& call)
1846 {
1847 FileInfo info;
1848 if (GetAttachmentInfo(info, call))
1849 {
1850 Json::Value operations = Json::arrayValue;
1851
1852 operations.append("compressed-data");
1853
1854 if (info.GetCompressedMD5() != "")
1855 {
1856 operations.append("compressed-md5");
1857 }
1858
1859 operations.append("compressed-size");
1860 operations.append("data");
1861
1862 if (info.GetUncompressedMD5() != "")
1863 {
1864 operations.append("md5");
1865 }
1866
1867 operations.append("size");
1868
1869 if (info.GetCompressedMD5() != "" &&
1870 info.GetUncompressedMD5() != "")
1871 {
1872 operations.append("verify-md5");
1873 }
1874
1875 call.GetOutput().AnswerJson(operations);
1876 }
1877 }
1878
1879
1880 template <int uncompress>
1881 static void GetAttachmentData(RestApi::GetCall& call)
1882 {
1883 ServerContext& context = OrthancRestApi::GetContext(call);
1884
1885 CheckValidResourceType(call);
1886
1887 std::string publicId = call.GetUriComponent("id", "");
1888 std::string name = call.GetUriComponent("name", "");
1889
1890 std::string content;
1891 context.ReadFile(content, publicId, StringToContentType(name),
1892 (uncompress == 1));
1893
1894 call.GetOutput().AnswerBuffer(content, "application/octet-stream");
1895 }
1896
1897
1898 static void GetAttachmentSize(RestApi::GetCall& call)
1899 {
1900 FileInfo info;
1901 if (GetAttachmentInfo(info, call))
1902 {
1903 call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetUncompressedSize()), "text/plain");
1904 }
1905 }
1906
1907
1908 static void GetAttachmentCompressedSize(RestApi::GetCall& call)
1909 {
1910 FileInfo info;
1911 if (GetAttachmentInfo(info, call))
1912 {
1913 call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetCompressedSize()), "text/plain");
1914 }
1915 }
1916
1917
1918 static void GetAttachmentMD5(RestApi::GetCall& call)
1919 {
1920 FileInfo info;
1921 if (GetAttachmentInfo(info, call) &&
1922 info.GetUncompressedMD5() != "")
1923 {
1924 call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetUncompressedMD5()), "text/plain");
1925 }
1926 }
1927
1928
1929 static void GetAttachmentCompressedMD5(RestApi::GetCall& call)
1930 {
1931 FileInfo info;
1932 if (GetAttachmentInfo(info, call) &&
1933 info.GetCompressedMD5() != "")
1934 {
1935 call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetCompressedMD5()), "text/plain");
1936 }
1937 }
1938
1939
1940 static void VerifyAttachment(RestApi::PostCall& call)
1941 {
1942 ServerContext& context = OrthancRestApi::GetContext(call);
1943 CheckValidResourceType(call);
1944
1945 std::string publicId = call.GetUriComponent("id", "");
1946 std::string name = call.GetUriComponent("name", "");
1947
1948 FileInfo info;
1949 if (!GetAttachmentInfo(info, call) ||
1950 info.GetCompressedMD5() == "" ||
1951 info.GetUncompressedMD5() == "")
1952 {
1953 // Inexistent resource, or no MD5 available
1954 return;
1955 }
1956
1957 bool ok = false;
1958
1959 // First check whether the compressed data is correctly stored in the disk
1960 std::string data;
1961 context.ReadFile(data, publicId, StringToContentType(name), false);
1962
1963 std::string actualMD5;
1964 Toolbox::ComputeMD5(actualMD5, data);
1965
1966 if (actualMD5 == info.GetCompressedMD5())
1967 {
1968 // The compressed data is OK. If a compression algorithm was
1969 // applied to it, now check the MD5 of the uncompressed data.
1970 if (info.GetCompressionType() == CompressionType_None)
1971 {
1972 ok = true;
1973 }
1974 else
1975 {
1976 context.ReadFile(data, publicId, StringToContentType(name), true);
1977 Toolbox::ComputeMD5(actualMD5, data);
1978 ok = (actualMD5 == info.GetUncompressedMD5());
1979 }
1980 }
1981
1982 if (ok)
1983 {
1984 LOG(INFO) << "The attachment " << name << " of resource " << publicId << " has the right MD5";
1985 call.GetOutput().AnswerBuffer("{}", "application/json");
1986 }
1987 else
1988 {
1989 LOG(INFO) << "The attachment " << name << " of resource " << publicId << " has bad MD5!";
1990 }
1991 }
1992
1993
1994 static void UploadAttachment(RestApi::PutCall& call)
1995 {
1996 ServerContext& context = OrthancRestApi::GetContext(call);
1997 CheckValidResourceType(call);
1998
1999 std::string publicId = call.GetUriComponent("id", "");
2000 std::string name = call.GetUriComponent("name", "");
2001
2002 const void* data = call.GetPutBody().size() ? &call.GetPutBody()[0] : NULL;
2003
2004 FileContentType contentType = StringToContentType(name);
2005 if (contentType >= FileContentType_StartUser && // It is forbidden to modify internal attachments
2006 contentType <= FileContentType_EndUser &&
2007 context.AddAttachment(publicId, StringToContentType(name), data, call.GetPutBody().size()))
2008 {
2009 call.GetOutput().AnswerBuffer("{}", "application/json");
2010 }
2011 }
2012
2013
2014 static void DeleteAttachment(RestApi::DeleteCall& call)
2015 {
2016 CheckValidResourceType(call);
2017
2018 std::string publicId = call.GetUriComponent("id", "");
2019 std::string name = call.GetUriComponent("name", "");
2020 FileContentType contentType = StringToContentType(name);
2021
2022 if (contentType >= FileContentType_StartUser &&
2023 contentType <= FileContentType_EndUser)
2024 {
2025 // It is forbidden to delete internal attachments
2026 OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType);
2027 call.GetOutput().AnswerBuffer("{}", "application/json");
2028 }
2029 }
2030
2031
2032
2033
2034 // Registration of the various REST handlers --------------------------------
2035
2036 OrthancRestApi::OrthancRestApi(ServerContext& context) :
2037 context_(context)
2038 {
2039 Register("/", ServeRoot);
2040 Register("/system", GetSystemInformation);
2041 Register("/statistics", GetStatistics);
2042 Register("/changes", GetChanges);
2043 Register("/changes", DeleteChanges);
2044 Register("/exports", GetExports);
2045 Register("/exports", DeleteExports);
2046
2047 Register("/instances", UploadDicomFile);
2048 Register("/instances", ListResources<ResourceType_Instance>);
2049 Register("/patients", ListResources<ResourceType_Patient>);
2050 Register("/series", ListResources<ResourceType_Series>);
2051 Register("/studies", ListResources<ResourceType_Study>);
2052
2053 Register("/instances/{id}", DeleteSingleResource<ResourceType_Instance>);
2054 Register("/instances/{id}", GetSingleResource<ResourceType_Instance>);
2055 Register("/patients/{id}", DeleteSingleResource<ResourceType_Patient>);
2056 Register("/patients/{id}", GetSingleResource<ResourceType_Patient>);
2057 Register("/series/{id}", DeleteSingleResource<ResourceType_Series>);
2058 Register("/series/{id}", GetSingleResource<ResourceType_Series>);
2059 Register("/studies/{id}", DeleteSingleResource<ResourceType_Study>);
2060 Register("/studies/{id}", GetSingleResource<ResourceType_Study>);
2061
2062 Register("/patients/{id}/archive", GetArchive<ResourceType_Patient>);
2063 Register("/studies/{id}/archive", GetArchive<ResourceType_Study>);
2064 Register("/series/{id}/archive", GetArchive<ResourceType_Series>);
2065
2066 Register("/instances/{id}/statistics", GetResourceStatistics);
2067 Register("/patients/{id}/statistics", GetResourceStatistics);
2068 Register("/studies/{id}/statistics", GetResourceStatistics);
2069 Register("/series/{id}/statistics", GetResourceStatistics);
2070
2071 Register("/patients/{id}/protected", IsProtectedPatient);
2072 Register("/patients/{id}/protected", SetPatientProtection);
2073 Register("/instances/{id}/file", GetInstanceFile);
2074 Register("/instances/{id}/export", ExportInstanceFile);
2075 Register("/instances/{id}/tags", GetInstanceTags<false>);
2076 Register("/instances/{id}/simplified-tags", GetInstanceTags<true>);
2077 Register("/instances/{id}/frames", ListFrames);
2078 Register("/instances/{id}/content/*", GetRawContent);
2079
2080 Register("/instances/{id}/frames/{frame}/preview", GetImage<ImageExtractionMode_Preview>);
2081 Register("/instances/{id}/frames/{frame}/image-uint8", GetImage<ImageExtractionMode_UInt8>);
2082 Register("/instances/{id}/frames/{frame}/image-uint16", GetImage<ImageExtractionMode_UInt16>);
2083 Register("/instances/{id}/frames/{frame}/image-int16", GetImage<ImageExtractionMode_Int16>);
2084 Register("/instances/{id}/preview", GetImage<ImageExtractionMode_Preview>);
2085 Register("/instances/{id}/image-uint8", GetImage<ImageExtractionMode_UInt8>);
2086 Register("/instances/{id}/image-uint16", GetImage<ImageExtractionMode_UInt16>);
2087 Register("/instances/{id}/image-int16", GetImage<ImageExtractionMode_Int16>);
2088
2089 Register("/modalities", ListModalities);
2090 Register("/modalities/{id}", ListModalityOperations);
2091 Register("/modalities/{id}/find-patient", DicomFindPatient);
2092 Register("/modalities/{id}/find-study", DicomFindStudy);
2093 Register("/modalities/{id}/find-series", DicomFindSeries);
2094 Register("/modalities/{id}/find-instance", DicomFindInstance);
2095 Register("/modalities/{id}/find", DicomFind);
2096 Register("/modalities/{id}/store", DicomStore);
2097
2098 Register("/peers", ListPeers);
2099 Register("/peers/{id}", ListPeerOperations);
2100 Register("/peers/{id}/store", PeerStore);
2101
2102 Register("/instances/{id}/modify", ModifyInstance);
2103 Register("/series/{id}/modify", ModifySeriesInplace);
2104 Register("/studies/{id}/modify", ModifyStudyInplace);
2105 //Register("/patients/{id}/modify", ModifyPatientInplace);
2106
2107 Register("/instances/{id}/anonymize", AnonymizeInstance);
2108 Register("/series/{id}/anonymize", AnonymizeSeriesInplace);
2109 Register("/studies/{id}/anonymize", AnonymizeStudyInplace);
2110 Register("/patients/{id}/anonymize", AnonymizePatientInplace);
2111
2112 Register("/tools/generate-uid", GenerateUid);
2113 Register("/tools/execute-script", ExecuteScript);
2114 Register("/tools/now", GetNowIsoString);
2115
2116 Register("/{resourceType}/{id}/metadata", ListMetadata);
2117 Register("/{resourceType}/{id}/metadata/{name}", DeleteMetadata);
2118 Register("/{resourceType}/{id}/metadata/{name}", GetMetadata);
2119 Register("/{resourceType}/{id}/metadata/{name}", SetMetadata);
2120
2121 Register("/{resourceType}/{id}/attachments", ListAttachments);
2122 Register("/{resourceType}/{id}/attachments/{name}", DeleteAttachment);
2123 Register("/{resourceType}/{id}/attachments/{name}", GetAttachmentOperations);
2124 Register("/{resourceType}/{id}/attachments/{name}/compressed-data", GetAttachmentData<0>);
2125 Register("/{resourceType}/{id}/attachments/{name}/compressed-md5", GetAttachmentCompressedMD5);
2126 Register("/{resourceType}/{id}/attachments/{name}/compressed-size", GetAttachmentCompressedSize);
2127 Register("/{resourceType}/{id}/attachments/{name}/data", GetAttachmentData<1>);
2128 Register("/{resourceType}/{id}/attachments/{name}/md5", GetAttachmentMD5);
2129 Register("/{resourceType}/{id}/attachments/{name}/size", GetAttachmentSize);
2130 Register("/{resourceType}/{id}/attachments/{name}/verify-md5", VerifyAttachment);
2131 Register("/{resourceType}/{id}/attachments/{name}", UploadAttachment);
2132 }
2133 }