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-2021 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 "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
38 #include "../../../OrthancFramework/Sources/Logging.h"
39 #include "../../../OrthancFramework/Sources/SerializationToolbox.h"
40 #include "../OrthancConfiguration.h"
41 #include "../ServerContext.h"
42 #include "../ServerJobs/MergeStudyJob.h"
43 #include "../ServerJobs/ResourceModificationJob.h"
44 #include "../ServerJobs/SplitStudyJob.h"
45 
46 #include <boost/lexical_cast.hpp>
47 #include <boost/algorithm/string/predicate.hpp>
48 
49 #define INFO_SUBSEQUENCES \
50   "Starting with Orthanc 1.9.4, paths to subsequences can be provided using the "\
51   "same syntax as the `dcmodify` command-line tool (wildcards are supported as well)."
52 
53 
54 static const char* const CONTENT = "Content";
55 static const char* const FORCE = "Force";
56 static const char* const INSTANCES = "Instances";
57 static const char* const INTERPRET_BINARY_TAGS = "InterpretBinaryTags";
58 static const char* const KEEP = "Keep";
59 static const char* const KEEP_PRIVATE_TAGS = "KeepPrivateTags";
60 static const char* const KEEP_SOURCE = "KeepSource";
61 static const char* const LEVEL = "Level";
62 static const char* const PARENT = "Parent";
63 static const char* const PRIVATE_CREATOR = "PrivateCreator";
64 static const char* const REMOVE = "Remove";
65 static const char* const REPLACE = "Replace";
66 static const char* const RESOURCES = "Resources";
67 static const char* const SERIES = "Series";
68 static const char* const TAGS = "Tags";
69 static const char* const TRANSCODE = "Transcode";
70 
71 
72 namespace Orthanc
73 {
74   // Modification of DICOM instances ------------------------------------------
75 
76 
GeneratePatientName(ServerContext & context)77   static std::string GeneratePatientName(ServerContext& context)
78   {
79     uint64_t seq = context.GetIndex().IncrementGlobalSequence(GlobalProperty_AnonymizationSequence, true /* shared */);
80     return "Anonymized" + boost::lexical_cast<std::string>(seq);
81   }
82 
83 
DocumentKeepSource(RestApiPostCall & call)84   static void DocumentKeepSource(RestApiPostCall& call)
85   {
86     call.GetDocumentation()
87       .SetRequestField(KEEP_SOURCE, RestApiCallDocumentation::Type_Boolean,
88                        "If set to `false`, instructs Orthanc to the remove original resources. "
89                        "By default, the original resources are kept in Orthanc.", false);
90   }
91 
92 
DocumentModifyOptions(RestApiPostCall & call)93   static void DocumentModifyOptions(RestApiPostCall& call)
94   {
95     // Check out "DicomModification::ParseModifyRequest()"
96     call.GetDocumentation()
97       .SetRequestField(TRANSCODE, RestApiCallDocumentation::Type_String,
98                        "Transcode the DICOM instances to the provided DICOM transfer syntax: "
99                        "https://book.orthanc-server.com/faq/transcoding.html", false)
100       .SetRequestField(FORCE, RestApiCallDocumentation::Type_Boolean,
101                        "Allow the modification of tags related to DICOM identifiers, at the risk of "
102                        "breaking the DICOM model of the real world", false)
103       .SetRequestField("RemovePrivateTags", RestApiCallDocumentation::Type_Boolean,
104                        "Remove the private tags from the DICOM instances (defaults to `false`)", false)
105       .SetRequestField(REPLACE, RestApiCallDocumentation::Type_JsonObject,
106                        "Associative array to change the value of some DICOM tags in the DICOM instances. " INFO_SUBSEQUENCES, false)
107       .SetRequestField(REMOVE, RestApiCallDocumentation::Type_JsonListOfStrings,
108                        "List of tags that must be removed from the DICOM instances. " INFO_SUBSEQUENCES, false)
109       .SetRequestField(KEEP, RestApiCallDocumentation::Type_JsonListOfStrings,
110                        "Keep the original value of the specified tags, to be chosen among the `StudyInstanceUID`, "
111                        "`SeriesInstanceUID` and `SOPInstanceUID` tags. Avoid this feature as much as possible, "
112                        "as this breaks the DICOM model of the real world.", false)
113       .SetRequestField(PRIVATE_CREATOR, RestApiCallDocumentation::Type_String,
114                        "The private creator to be used for private tags in `Replace`", false);
115 
116     // This was existing, but undocumented in Orthanc <= 1.9.6
117     DocumentKeepSource(call);
118   }
119 
120 
DocumentAnonymizationOptions(RestApiPostCall & call)121   static void DocumentAnonymizationOptions(RestApiPostCall& call)
122   {
123     // Check out "DicomModification::ParseAnonymizationRequest()"
124     call.GetDocumentation()
125       .SetRequestField(FORCE, RestApiCallDocumentation::Type_Boolean,
126                        "Allow the modification of tags related to DICOM identifiers, at the risk of "
127                        "breaking the DICOM model of the real world", false)
128       .SetRequestField("DicomVersion", RestApiCallDocumentation::Type_String,
129                        "Version of the DICOM standard to be used for anonymization. Check out "
130                        "configuration option `DeidentifyLogsDicomVersion` for possible values.", false)
131       .SetRequestField(KEEP_PRIVATE_TAGS, RestApiCallDocumentation::Type_Boolean,
132                        "Keep the private tags from the DICOM instances (defaults to `false`)", false)
133       .SetRequestField(REPLACE, RestApiCallDocumentation::Type_JsonObject,
134                        "Associative array to change the value of some DICOM tags in the DICOM instances. " INFO_SUBSEQUENCES, false)
135       .SetRequestField(REMOVE, RestApiCallDocumentation::Type_JsonListOfStrings,
136                        "List of additional tags to be removed from the DICOM instances. " INFO_SUBSEQUENCES, false)
137       .SetRequestField(KEEP, RestApiCallDocumentation::Type_JsonListOfStrings,
138                        "List of DICOM tags whose value must not be destroyed by the anonymization. " INFO_SUBSEQUENCES, false)
139       .SetRequestField(PRIVATE_CREATOR, RestApiCallDocumentation::Type_String,
140                        "The private creator to be used for private tags in `Replace`", false);
141 
142     // This was existing, but undocumented in Orthanc <= 1.9.6
143     DocumentKeepSource(call);
144   }
145 
146 
ParseModifyRequest(Json::Value & request,DicomModification & target,const RestApiPostCall & call)147   static void ParseModifyRequest(Json::Value& request,
148                                  DicomModification& target,
149                                  const RestApiPostCall& call)
150   {
151     // curl http://localhost:8042/series/95a6e2bf-9296e2cc-bf614e2f-22b391ee-16e010e0/modify -X POST -d '{"Replace":{"InstitutionName":"My own clinic"},"Priority":9}'
152 
153     {
154       OrthancConfiguration::ReaderLock lock;
155       target.SetPrivateCreator(lock.GetConfiguration().GetDefaultPrivateCreator());
156     }
157 
158     if (call.ParseJsonRequest(request))
159     {
160       target.ParseModifyRequest(request);
161     }
162     else
163     {
164       throw OrthancException(ErrorCode_BadFileFormat);
165     }
166   }
167 
168 
ParseAnonymizationRequest(Json::Value & request,DicomModification & target,RestApiPostCall & call)169   static void ParseAnonymizationRequest(Json::Value& request,
170                                         DicomModification& target,
171                                         RestApiPostCall& call)
172   {
173     // curl http://localhost:8042/instances/6e67da51-d119d6ae-c5667437-87b9a8a5-0f07c49f/anonymize -X POST -d '{"Replace":{"PatientName":"hello","0010-0020":"world"},"Keep":["StudyDescription", "SeriesDescription"],"KeepPrivateTags": true,"Remove":["Modality"]}' > Anonymized.dcm
174 
175     {
176       OrthancConfiguration::ReaderLock lock;
177       target.SetPrivateCreator(lock.GetConfiguration().GetDefaultPrivateCreator());
178     }
179 
180     if (call.ParseJsonRequest(request) &&
181         request.isObject())
182     {
183       bool patientNameOverridden;
184       target.ParseAnonymizationRequest(patientNameOverridden, request);
185 
186       if (!patientNameOverridden)
187       {
188         // Override the random Patient's Name by one that is more
189         // user-friendly (provided none was specified by the user)
190         target.Replace(DICOM_TAG_PATIENT_NAME, GeneratePatientName(OrthancRestApi::GetContext(call)), true);
191       }
192     }
193     else
194     {
195       throw OrthancException(ErrorCode_BadFileFormat);
196     }
197   }
198 
199 
AnonymizeOrModifyInstance(DicomModification & modification,RestApiPostCall & call,bool transcode,DicomTransferSyntax targetSyntax)200   static void AnonymizeOrModifyInstance(DicomModification& modification,
201                                         RestApiPostCall& call,
202                                         bool transcode,
203                                         DicomTransferSyntax targetSyntax)
204   {
205     ServerContext& context = OrthancRestApi::GetContext(call);
206     std::string id = call.GetUriComponent("id", "");
207 
208     std::unique_ptr<ParsedDicomFile> modified;
209 
210     {
211       ServerContext::DicomCacheLocker locker(context, id);
212       modified.reset(locker.GetDicom().Clone(true));
213     }
214 
215     modification.Apply(*modified);
216 
217     if (transcode)
218     {
219       IDicomTranscoder::DicomImage source;
220       source.AcquireParsed(*modified);  // "modified" is invalid below this point
221 
222       IDicomTranscoder::DicomImage transcoded;
223 
224       std::set<DicomTransferSyntax> s;
225       s.insert(targetSyntax);
226 
227       if (context.Transcode(transcoded, source, s, true))
228       {
229         call.GetOutput().AnswerBuffer(transcoded.GetBufferData(),
230                                       transcoded.GetBufferSize(), MimeType_Dicom);
231       }
232       else
233       {
234         throw OrthancException(ErrorCode_InternalError,
235                                "Cannot transcode to transfer syntax: " +
236                                std::string(GetTransferSyntaxUid(targetSyntax)));
237       }
238     }
239     else
240     {
241       modified->Answer(call.GetOutput());
242     }
243   }
244 
245 
DetectModifyLevel(const DicomModification & modification)246   static ResourceType DetectModifyLevel(const DicomModification& modification)
247   {
248     if (modification.IsReplaced(DICOM_TAG_PATIENT_ID))
249     {
250       return ResourceType_Patient;
251     }
252     else if (modification.IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
253     {
254       return ResourceType_Study;
255     }
256     else if (modification.IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
257     {
258       return ResourceType_Series;
259     }
260     else
261     {
262       return ResourceType_Instance;
263     }
264   }
265 
266 
ModifyInstance(RestApiPostCall & call)267   static void ModifyInstance(RestApiPostCall& call)
268   {
269     if (call.IsDocumentation())
270     {
271       DocumentModifyOptions(call);
272       call.GetDocumentation()
273         .SetTag("Instances")
274         .SetSummary("Modify instance")
275         .SetDescription("Download a modified version of the DICOM instance whose Orthanc identifier is provided in the URL: "
276                         "https://book.orthanc-server.com/users/anonymization.html#modification-of-a-single-instance")
277         .SetUriArgument("id", "Orthanc identifier of the instance of interest")
278         .AddAnswerType(MimeType_Dicom, "The modified DICOM instance");
279       return;
280     }
281 
282     DicomModification modification;
283     modification.SetAllowManualIdentifiers(true);
284 
285     Json::Value request;
286     ParseModifyRequest(request, modification, call);
287 
288     modification.SetLevel(DetectModifyLevel(modification));
289 
290     if (request.isMember(TRANSCODE))
291     {
292       std::string s = SerializationToolbox::ReadString(request, TRANSCODE);
293 
294       DicomTransferSyntax syntax;
295       if (LookupTransferSyntax(syntax, s))
296       {
297         AnonymizeOrModifyInstance(modification, call, true, syntax);
298       }
299       else
300       {
301         throw OrthancException(ErrorCode_ParameterOutOfRange, "Unknown transfer syntax: " + s);
302       }
303     }
304     else
305     {
306       AnonymizeOrModifyInstance(modification, call, false /* no transcoding */,
307                                 DicomTransferSyntax_LittleEndianImplicit /* unused */);
308     }
309   }
310 
311 
AnonymizeInstance(RestApiPostCall & call)312   static void AnonymizeInstance(RestApiPostCall& call)
313   {
314     if (call.IsDocumentation())
315     {
316       DocumentAnonymizationOptions(call);
317       call.GetDocumentation()
318         .SetTag("Instances")
319         .SetSummary("Anonymize instance")
320         .SetDescription("Download an anonymized version of the DICOM instance whose Orthanc identifier is provided in the URL: "
321                         "https://book.orthanc-server.com/users/anonymization.html#anonymization-of-a-single-instance")
322         .SetUriArgument("id", "Orthanc identifier of the instance of interest")
323         .AddAnswerType(MimeType_Dicom, "The anonymized DICOM instance");
324       return;
325     }
326 
327     DicomModification modification;
328     modification.SetAllowManualIdentifiers(true);
329 
330     Json::Value request;
331     ParseAnonymizationRequest(request, modification, call);
332 
333     AnonymizeOrModifyInstance(modification, call, false /* no transcoding */,
334                               DicomTransferSyntax_LittleEndianImplicit /* unused */);
335   }
336 
337 
SetKeepSource(CleaningInstancesJob & job,const Json::Value & body)338   static void SetKeepSource(CleaningInstancesJob& job,
339                             const Json::Value& body)
340   {
341     if (body.isMember(KEEP_SOURCE))
342     {
343       job.SetKeepSource(SerializationToolbox::ReadBoolean(body, KEEP_SOURCE));
344     }
345   }
346 
347 
SubmitModificationJob(std::unique_ptr<DicomModification> & modification,bool isAnonymization,RestApiPostCall & call,const Json::Value & body,ResourceType outputLevel,bool isSingleResource,const std::set<std::string> & resources)348   static void SubmitModificationJob(std::unique_ptr<DicomModification>& modification,
349                                     bool isAnonymization,
350                                     RestApiPostCall& call,
351                                     const Json::Value& body,
352                                     ResourceType outputLevel /* unused for multiple resources */,
353                                     bool isSingleResource,
354                                     const std::set<std::string>& resources)
355   {
356     ServerContext& context = OrthancRestApi::GetContext(call);
357 
358     std::unique_ptr<ResourceModificationJob> job(new ResourceModificationJob(context));
359 
360     if (isSingleResource)  // This notably configures the output format
361     {
362       job->SetSingleResourceModification(modification.release(), outputLevel, isAnonymization);
363     }
364     else
365     {
366       job->SetMultipleResourcesModification(modification.release(), isAnonymization);
367     }
368 
369     job->SetOrigin(call);
370     SetKeepSource(*job, body);
371 
372     if (body.isMember(TRANSCODE))
373     {
374       job->SetTranscode(SerializationToolbox::ReadString(body, TRANSCODE));
375     }
376 
377     for (std::set<std::string>::const_iterator
378            it = resources.begin(); it != resources.end(); ++it)
379     {
380       context.AddChildInstances(*job, *it);
381     }
382 
383     job->AddTrailingStep();
384 
385     OrthancRestApi::GetApi(call).SubmitCommandsJob
386       (call, job.release(), true /* synchronous by default */, body);
387   }
388 
389 
SubmitModificationJob(std::unique_ptr<DicomModification> & modification,bool isAnonymization,RestApiPostCall & call,const Json::Value & body,ResourceType outputLevel)390   static void SubmitModificationJob(std::unique_ptr<DicomModification>& modification,
391                                     bool isAnonymization,
392                                     RestApiPostCall& call,
393                                     const Json::Value& body,
394                                     ResourceType outputLevel)
395   {
396     // This was the only flavor in Orthanc <= 1.9.3
397     std::set<std::string> resources;
398     resources.insert(call.GetUriComponent("id", ""));
399 
400     SubmitModificationJob(modification, isAnonymization, call, body, outputLevel,
401                           true /* single resource */, resources);
402   }
403 
404 
SubmitBulkJob(std::unique_ptr<DicomModification> & modification,bool isAnonymization,RestApiPostCall & call,const Json::Value & body)405   static void SubmitBulkJob(std::unique_ptr<DicomModification>& modification,
406                             bool isAnonymization,
407                             RestApiPostCall& call,
408                             const Json::Value& body)
409   {
410     std::set<std::string> resources;
411     SerializationToolbox::ReadSetOfStrings(resources, body, RESOURCES);
412 
413     SubmitModificationJob(modification, isAnonymization,
414                           call, body, ResourceType_Instance /* arbitrary value, unused */,
415                           false /* multiple resources */, resources);
416   }
417 
418 
419   template <enum ResourceType resourceType>
ModifyResource(RestApiPostCall & call)420   static void ModifyResource(RestApiPostCall& call)
421   {
422     if (call.IsDocumentation())
423     {
424       OrthancRestApi::DocumentSubmitCommandsJob(call);
425       DocumentModifyOptions(call);
426       const std::string r = GetResourceTypeText(resourceType, false /* plural */, false /* lower case */);
427       call.GetDocumentation()
428         .SetTag(GetResourceTypeText(resourceType, true /* plural */, true /* upper case */))
429         .SetSummary("Modify " + r)
430         .SetDescription("Start a job that will modify all the DICOM instances within the " + r +
431                         " whose identifier is provided in the URL. The modified DICOM instances will be "
432                         "stored into a brand new " + r + ", whose Orthanc identifiers will be returned by the job. "
433                         "https://book.orthanc-server.com/users/anonymization.html#modification-of-studies-or-series")
434         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest");
435       return;
436     }
437 
438     std::unique_ptr<DicomModification> modification(new DicomModification);
439 
440     Json::Value body;
441     ParseModifyRequest(body, *modification, call);
442 
443     modification->SetLevel(resourceType);
444 
445     SubmitModificationJob(modification, false /* not an anonymization */,
446                           call, body, resourceType);
447   }
448 
449 
450   // New in Orthanc 1.9.4
BulkModify(RestApiPostCall & call)451   static void BulkModify(RestApiPostCall& call)
452   {
453     if (call.IsDocumentation())
454     {
455       OrthancRestApi::DocumentSubmitCommandsJob(call);
456       DocumentModifyOptions(call);
457       call.GetDocumentation()
458         .SetTag("System")
459         .SetSummary("Modify a set of resources")
460         .SetRequestField(RESOURCES, RestApiCallDocumentation::Type_JsonListOfStrings,
461                          "List of the Orthanc identifiers of the patients/studies/series/instances of interest.", true)
462         .SetRequestField(LEVEL, RestApiCallDocumentation::Type_String,
463                          "Level of the modification (`Patient`, `Study`, `Series` or `Instance`). If absent, "
464                          "the level defaults to `Instance`, but is set to `Patient` if `PatientID` is modified, "
465                          "to `Study` if `StudyInstanceUID` is modified, or to `Series` if `SeriesInstancesUID` "
466                          "is modified. (new in Orthanc 1.9.7)", false)
467         .SetDescription("Start a job that will modify all the DICOM patients, studies, series or instances "
468                         "whose identifiers are provided in the `Resources` field.")
469         .AddAnswerType(MimeType_Json, "The list of all the resources that have been altered by this modification");
470       return;
471     }
472 
473     std::unique_ptr<DicomModification> modification(new DicomModification);
474 
475     Json::Value body;
476     ParseModifyRequest(body, *modification, call);
477 
478     if (body.isMember(LEVEL))
479     {
480       // This case was introduced in Orthanc 1.9.7
481       modification->SetLevel(StringToResourceType(body[LEVEL].asCString()));
482     }
483     else
484     {
485       modification->SetLevel(DetectModifyLevel(*modification));
486     }
487 
488     SubmitBulkJob(modification, false /* not an anonymization */, call, body);
489   }
490 
491 
492   template <enum ResourceType resourceType>
AnonymizeResource(RestApiPostCall & call)493   static void AnonymizeResource(RestApiPostCall& call)
494   {
495     if (call.IsDocumentation())
496     {
497       OrthancRestApi::DocumentSubmitCommandsJob(call);
498       DocumentAnonymizationOptions(call);
499       const std::string r = GetResourceTypeText(resourceType, false /* plural */, false /* lower case */);
500       call.GetDocumentation()
501         .SetTag(GetResourceTypeText(resourceType, true /* plural */, true /* upper case */))
502         .SetSummary("Anonymize " + r)
503         .SetDescription("Start a job that will anonymize all the DICOM instances within the " + r +
504                         " whose identifier is provided in the URL. The modified DICOM instances will be "
505                         "stored into a brand new " + r + ", whose Orthanc identifiers will be returned by the job. "
506                         "https://book.orthanc-server.com/users/anonymization.html#anonymization-of-patients-studies-or-series")
507         .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest");
508       return;
509     }
510 
511     std::unique_ptr<DicomModification> modification(new DicomModification);
512 
513     Json::Value body;
514     ParseAnonymizationRequest(body, *modification, call);
515 
516     SubmitModificationJob(modification, true /* anonymization */,
517                           call, body, resourceType);
518   }
519 
520 
521   // New in Orthanc 1.9.4
BulkAnonymize(RestApiPostCall & call)522   static void BulkAnonymize(RestApiPostCall& call)
523   {
524     if (call.IsDocumentation())
525     {
526       OrthancRestApi::DocumentSubmitCommandsJob(call);
527       DocumentAnonymizationOptions(call);
528       call.GetDocumentation()
529         .SetTag("System")
530         .SetSummary("Anonymize a set of resources")
531         .SetRequestField(RESOURCES, RestApiCallDocumentation::Type_JsonListOfStrings,
532                          "List of the Orthanc identifiers of the patients/studies/series/instances of interest.", true)
533         .SetDescription("Start a job that will anonymize all the DICOM patients, studies, series or instances "
534                         "whose identifiers are provided in the `Resources` field.")
535         .AddAnswerType(MimeType_Json, "The list of all the resources that have been created by this anonymization");
536       return;
537     }
538 
539     std::unique_ptr<DicomModification> modification(new DicomModification);
540 
541     Json::Value body;
542     ParseAnonymizationRequest(body, *modification, call);
543 
544     SubmitBulkJob(modification, true /* anonymization */, call, body);
545   }
546 
547 
StoreCreatedInstance(std::string & id,RestApiPostCall & call,ParsedDicomFile & dicom,bool sendAnswer)548   static void StoreCreatedInstance(std::string& id /* out */,
549                                    RestApiPostCall& call,
550                                    ParsedDicomFile& dicom,
551                                    bool sendAnswer)
552   {
553     std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromParsedDicomFile(dicom));
554     toStore->SetOrigin(DicomInstanceOrigin::FromRest(call));
555 
556     ServerContext& context = OrthancRestApi::GetContext(call);
557     StoreStatus status = context.Store(id, *toStore, StoreInstanceMode_Default);
558 
559     if (status == StoreStatus_Failure)
560     {
561       throw OrthancException(ErrorCode_CannotStoreInstance);
562     }
563 
564     if (sendAnswer)
565     {
566       OrthancRestApi::GetApi(call).AnswerStoredInstance(call, *toStore, status, id);
567     }
568   }
569 
570 
CreateDicomV1(ParsedDicomFile & dicom,RestApiPostCall & call,const Json::Value & request)571   static void CreateDicomV1(ParsedDicomFile& dicom,
572                             RestApiPostCall& call,
573                             const Json::Value& request)
574   {
575     // curl http://localhost:8042/tools/create-dicom -X POST -d '{"PatientName":"Hello^World"}'
576     // curl http://localhost:8042/tools/create-dicom -X POST -d '{"PatientName":"Hello^World","PixelData":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDDcB53FulQAAAElJREFUGNNtj0sSAEEEQ1+U+185s1CtmRkblQ9CZldsKHJDk6DLGLJa6chjh0ooQmpjXMM86zPwydGEj6Ed/UGykkEM8X+p3u8/8LcOJIWLGeMAAAAASUVORK5CYII="}'
577 
578     assert(request.isObject());
579     LOG(WARNING) << "Using a deprecated call to /tools/create-dicom";
580 
581     Json::Value::Members members = request.getMemberNames();
582     for (size_t i = 0; i < members.size(); i++)
583     {
584       const std::string& name = members[i];
585       if (request[name].type() != Json::stringValue)
586       {
587         throw OrthancException(ErrorCode_CreateDicomNotString);
588       }
589 
590       std::string value = request[name].asString();
591 
592       DicomTag tag = FromDcmtkBridge::ParseTag(name);
593       if (tag == DICOM_TAG_PIXEL_DATA)
594       {
595         dicom.EmbedContent(value);
596       }
597       else
598       {
599         // This is V1, don't try and decode data URI scheme
600         dicom.ReplacePlainString(tag, value);
601       }
602     }
603   }
604 
605 
InjectTags(ParsedDicomFile & dicom,const Json::Value & tags,bool decodeBinaryTags,const std::string & privateCreator,bool force)606   static void InjectTags(ParsedDicomFile& dicom,
607                          const Json::Value& tags,
608                          bool decodeBinaryTags,
609                          const std::string& privateCreator,
610                          bool force)
611   {
612     if (tags.type() != Json::objectValue)
613     {
614       throw OrthancException(ErrorCode_BadRequest, "Tags field is not an array");
615     }
616 
617     // Inject the user-specified tags
618     Json::Value::Members members = tags.getMemberNames();
619     for (size_t i = 0; i < members.size(); i++)
620     {
621       const std::string& name = members[i];
622       DicomTag tag = FromDcmtkBridge::ParseTag(name);
623 
624       if (tag != DICOM_TAG_SPECIFIC_CHARACTER_SET)
625       {
626         if (!force &&
627             tag != DICOM_TAG_PATIENT_ID &&
628             tag != DICOM_TAG_ACQUISITION_DATE &&
629             tag != DICOM_TAG_ACQUISITION_TIME &&
630             tag != DICOM_TAG_CONTENT_DATE &&
631             tag != DICOM_TAG_CONTENT_TIME &&
632             tag != DICOM_TAG_INSTANCE_CREATION_DATE &&
633             tag != DICOM_TAG_INSTANCE_CREATION_TIME &&
634             tag != DICOM_TAG_SERIES_DATE &&
635             tag != DICOM_TAG_SERIES_TIME &&
636             tag != DICOM_TAG_STUDY_DATE &&
637             tag != DICOM_TAG_STUDY_TIME &&
638             dicom.HasTag(tag))
639         {
640           throw OrthancException(ErrorCode_CreateDicomOverrideTag, name);
641         }
642 
643         if (tag == DICOM_TAG_PIXEL_DATA)
644         {
645           throw OrthancException(ErrorCode_CreateDicomUseContent);
646         }
647         else
648         {
649           dicom.Replace(tag, tags[name], decodeBinaryTags, DicomReplaceMode_InsertIfAbsent, privateCreator);
650         }
651       }
652     }
653   }
654 
655 
CreateSeries(RestApiPostCall & call,ParsedDicomFile & base,const Json::Value & content,bool decodeBinaryTags,const std::string & privateCreator,bool force)656   static void CreateSeries(RestApiPostCall& call,
657                            ParsedDicomFile& base /* in */,
658                            const Json::Value& content,
659                            bool decodeBinaryTags,
660                            const std::string& privateCreator,
661                            bool force)
662   {
663     assert(content.isArray());
664     assert(content.size() > 0);
665     ServerContext& context = OrthancRestApi::GetContext(call);
666 
667     base.ReplacePlainString(DICOM_TAG_IMAGES_IN_ACQUISITION, boost::lexical_cast<std::string>(content.size()));
668     base.ReplacePlainString(DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS, "1");
669 
670     std::string someInstance;
671 
672     try
673     {
674       for (Json::ArrayIndex i = 0; i < content.size(); i++)
675       {
676         std::unique_ptr<ParsedDicomFile> dicom(base.Clone(false));
677         const Json::Value* payload = NULL;
678 
679         if (content[i].type() == Json::stringValue)
680         {
681           payload = &content[i];
682         }
683         else if (content[i].type() == Json::objectValue)
684         {
685           if (!content[i].isMember(CONTENT))
686           {
687             throw OrthancException(ErrorCode_CreateDicomNoPayload);
688           }
689 
690           payload = &content[i][CONTENT];
691 
692           if (content[i].isMember(TAGS))
693           {
694             InjectTags(*dicom, content[i][TAGS], decodeBinaryTags, privateCreator, force);
695           }
696         }
697 
698         if (payload == NULL ||
699             payload->type() != Json::stringValue)
700         {
701           throw OrthancException(ErrorCode_CreateDicomUseDataUriScheme);
702         }
703 
704         dicom->EmbedContent(payload->asString());
705         dicom->ReplacePlainString(DICOM_TAG_INSTANCE_NUMBER, boost::lexical_cast<std::string>(i + 1));
706         dicom->ReplacePlainString(DICOM_TAG_IMAGE_INDEX, boost::lexical_cast<std::string>(i + 1));
707 
708         StoreCreatedInstance(someInstance, call, *dicom, false);
709       }
710     }
711     catch (OrthancException&)
712     {
713       // Error: Remove the newly-created series
714 
715       std::string series;
716       if (context.GetIndex().LookupParent(series, someInstance))
717       {
718         Json::Value dummy;
719         context.GetIndex().DeleteResource(dummy, series, ResourceType_Series);
720       }
721 
722       throw;
723     }
724 
725     std::string series;
726     if (context.GetIndex().LookupParent(series, someInstance))
727     {
728       OrthancRestApi::GetApi(call).AnswerStoredResource(call, series, ResourceType_Series, StoreStatus_Success);
729     }
730   }
731 
732 
CreateDicomV2(RestApiPostCall & call,const Json::Value & request)733   static void CreateDicomV2(RestApiPostCall& call,
734                             const Json::Value& request)
735   {
736     static const char* const SPECIFIC_CHARACTER_SET_2 = "SpecificCharacterSet";
737     static const char* const TYPE = "Type";
738     static const char* const VALUE = "Value";
739 
740     assert(request.isObject());
741     ServerContext& context = OrthancRestApi::GetContext(call);
742 
743     if (!request.isMember(TAGS) ||
744         request[TAGS].type() != Json::objectValue)
745     {
746       throw OrthancException(ErrorCode_BadRequest);
747     }
748 
749     ParsedDicomFile dicom(true);
750 
751     {
752       Encoding encoding;
753 
754       if (request[TAGS].isMember(SPECIFIC_CHARACTER_SET_2))
755       {
756         const char* tmp = request[TAGS][SPECIFIC_CHARACTER_SET_2].asCString();
757         if (!GetDicomEncoding(encoding, tmp))
758         {
759           throw OrthancException(ErrorCode_ParameterOutOfRange,
760                                  "Unknown specific character set: " + std::string(tmp));
761         }
762       }
763       else
764       {
765         encoding = GetDefaultDicomEncoding();
766       }
767 
768       dicom.SetEncoding(encoding);
769     }
770 
771     ResourceType parentType = ResourceType_Instance;
772 
773     if (request.isMember(PARENT))
774     {
775       // Locate the parent tags
776       std::string parent = request[PARENT].asString();
777       if (!context.GetIndex().LookupResourceType(parentType, parent))
778       {
779         throw OrthancException(ErrorCode_CreateDicomBadParent);
780       }
781 
782       if (parentType == ResourceType_Instance)
783       {
784         throw OrthancException(ErrorCode_CreateDicomParentIsInstance);
785       }
786 
787       // Select one existing child instance of the parent resource, to
788       // retrieve all its tags
789       Json::Value siblingTags;
790       std::string siblingInstanceId;
791 
792       {
793         // Retrieve all the instances of the parent resource
794         std::list<std::string>  siblingInstances;
795         context.GetIndex().GetChildInstances(siblingInstances, parent);
796 
797         if (siblingInstances.empty())
798 	{
799 	  // Error: No instance (should never happen)
800           throw OrthancException(ErrorCode_InternalError);
801         }
802 
803         siblingInstanceId = siblingInstances.front();
804         context.ReadDicomAsJson(siblingTags, siblingInstanceId);
805       }
806 
807 
808       // Choose the same encoding as the parent resource
809       {
810         static const char* const SPECIFIC_CHARACTER_SET = "0008,0005";
811 
812         if (siblingTags.isMember(SPECIFIC_CHARACTER_SET))
813         {
814           Encoding encoding;
815 
816           if (!siblingTags[SPECIFIC_CHARACTER_SET].isMember(VALUE) ||
817               siblingTags[SPECIFIC_CHARACTER_SET][VALUE].type() != Json::stringValue ||
818               !GetDicomEncoding(encoding, siblingTags[SPECIFIC_CHARACTER_SET][VALUE].asCString()))
819           {
820             LOG(WARNING) << "Instance with an incorrect Specific Character Set, "
821                          << "using the default Orthanc encoding: " << siblingInstanceId;
822             encoding = GetDefaultDicomEncoding();
823           }
824 
825           dicom.SetEncoding(encoding);
826         }
827       }
828 
829 
830       // Retrieve the tags for all the parent modules
831       typedef std::set<DicomTag> ModuleTags;
832       ModuleTags moduleTags;
833 
834       ResourceType type = parentType;
835       for (;;)
836       {
837         DicomTag::AddTagsForModule(moduleTags, GetModule(type));
838 
839         if (type == ResourceType_Patient)
840         {
841           break;   // We're done
842         }
843 
844         // Go up
845         std::string tmp;
846         if (!context.GetIndex().LookupParent(tmp, parent))
847         {
848           throw OrthancException(ErrorCode_InternalError);
849         }
850 
851         parent = tmp;
852         type = GetParentResourceType(type);
853       }
854 
855       for (ModuleTags::const_iterator it = moduleTags.begin();
856            it != moduleTags.end(); ++it)
857       {
858         std::string t = it->Format();
859         if (siblingTags.isMember(t))
860         {
861           const Json::Value& tag = siblingTags[t];
862           if (tag[TYPE] == "Null")
863           {
864             dicom.ReplacePlainString(*it, "");
865           }
866           else if (tag[TYPE] == "String")
867           {
868             std::string value = tag[VALUE].asString();  // This is an UTF-8 value (as it comes from JSON)
869             dicom.ReplacePlainString(*it, value);
870           }
871         }
872       }
873     }
874 
875 
876     bool decodeBinaryTags = true;
877     if (request.isMember(INTERPRET_BINARY_TAGS))
878     {
879       const Json::Value& v = request[INTERPRET_BINARY_TAGS];
880       if (v.type() != Json::booleanValue)
881       {
882         throw OrthancException(ErrorCode_BadRequest);
883       }
884 
885       decodeBinaryTags = v.asBool();
886     }
887 
888 
889     // New argument in Orthanc 1.6.0
890     std::string privateCreator;
891     if (request.isMember(PRIVATE_CREATOR))
892     {
893       const Json::Value& v = request[PRIVATE_CREATOR];
894       if (v.type() != Json::stringValue)
895       {
896         throw OrthancException(ErrorCode_BadRequest);
897       }
898 
899       privateCreator = v.asString();
900     }
901     else
902     {
903       OrthancConfiguration::ReaderLock lock;
904       privateCreator = lock.GetConfiguration().GetDefaultPrivateCreator();
905     }
906 
907 
908     // New in Orthanc 1.9.0
909     bool force = false;
910     if (request.isMember(FORCE))
911     {
912       const Json::Value& v = request[FORCE];
913       if (v.type() != Json::booleanValue)
914       {
915         throw OrthancException(ErrorCode_BadRequest);
916       }
917 
918       force = v.asBool();
919     }
920 
921 
922     // Inject time-related information
923     std::string date, time;
924     SystemToolbox::GetNowDicom(date, time, true /* use UTC time (not local time) */);
925     dicom.ReplacePlainString(DICOM_TAG_ACQUISITION_DATE, date);
926     dicom.ReplacePlainString(DICOM_TAG_ACQUISITION_TIME, time);
927     dicom.ReplacePlainString(DICOM_TAG_CONTENT_DATE, date);
928     dicom.ReplacePlainString(DICOM_TAG_CONTENT_TIME, time);
929     dicom.ReplacePlainString(DICOM_TAG_INSTANCE_CREATION_DATE, date);
930     dicom.ReplacePlainString(DICOM_TAG_INSTANCE_CREATION_TIME, time);
931 
932     if (parentType == ResourceType_Patient ||
933         parentType == ResourceType_Study ||
934         parentType == ResourceType_Instance /* no parent */)
935     {
936       dicom.ReplacePlainString(DICOM_TAG_SERIES_DATE, date);
937       dicom.ReplacePlainString(DICOM_TAG_SERIES_TIME, time);
938     }
939 
940     if (parentType == ResourceType_Patient ||
941         parentType == ResourceType_Instance /* no parent */)
942     {
943       dicom.ReplacePlainString(DICOM_TAG_STUDY_DATE, date);
944       dicom.ReplacePlainString(DICOM_TAG_STUDY_TIME, time);
945     }
946 
947 
948     InjectTags(dicom, request[TAGS], decodeBinaryTags, privateCreator, force);
949 
950 
951     // Inject the content (either an image, or a PDF file)
952     if (request.isMember(CONTENT))
953     {
954       const Json::Value& content = request[CONTENT];
955 
956       if (content.type() == Json::stringValue)
957       {
958         dicom.EmbedContent(request[CONTENT].asString());
959 
960       }
961       else if (content.type() == Json::arrayValue)
962       {
963         if (content.size() > 0)
964         {
965           // Let's create a series instead of a single instance
966           CreateSeries(call, dicom, content, decodeBinaryTags, privateCreator, force);
967           return;
968         }
969       }
970       else
971       {
972         throw OrthancException(ErrorCode_CreateDicomUseDataUriScheme);
973       }
974     }
975 
976     std::string id;
977     StoreCreatedInstance(id, call, dicom, true);
978   }
979 
980 
CreateDicom(RestApiPostCall & call)981   static void CreateDicom(RestApiPostCall& call)
982   {
983     if (call.IsDocumentation())
984     {
985       call.GetDocumentation()
986         .SetTag("System")
987         .SetSummary("Create one DICOM instance")
988         .SetDescription("Create one DICOM instance, and store it into Orthanc")
989         .SetRequestField(TAGS, RestApiCallDocumentation::Type_JsonObject,
990                          "Associative array containing the tags of the new instance to be created", true)
991         .SetRequestField(CONTENT, RestApiCallDocumentation::Type_String,
992                          "This field can be used to embed an image (pixel data) or a PDF inside the created DICOM instance. "
993                          "The PNG image, the JPEG image or the PDF file must be provided using their "
994                          "[data URI scheme encoding](https://en.wikipedia.org/wiki/Data_URI_scheme). "
995                          "This field can possibly contain a JSON array, in which case a DICOM series is created "
996                          "containing one DICOM instance for each item in the `Content` field.", false)
997         .SetRequestField(PARENT, RestApiCallDocumentation::Type_String,
998                          "If present, the newly created instance will be attached to the parent DICOM resource "
999                          "whose Orthanc identifier is contained in this field. The DICOM tags of the parent "
1000                          "modules in the DICOM hierarchy will be automatically copied to the newly created instance.", false)
1001         .SetRequestField(INTERPRET_BINARY_TAGS, RestApiCallDocumentation::Type_Boolean,
1002                          "If some value in the `Tags` associative array is formatted according to some "
1003                          "[data URI scheme encoding](https://en.wikipedia.org/wiki/Data_URI_scheme), "
1004                          "whether this value is decoded to a binary value or kept as such (`true` by default)", false)
1005         .SetRequestField(PRIVATE_CREATOR, RestApiCallDocumentation::Type_String,
1006                          "The private creator to be used for private tags in `Tags`", false)
1007         .SetRequestField(FORCE, RestApiCallDocumentation::Type_Boolean,
1008                          "Avoid the consistency checks for the DICOM tags that enforce the DICOM model of the real-world. "
1009                          "You can notably use this flag if you need to manually set the tags `StudyInstanceUID`, "
1010                          "`SeriesInstanceUID`, or `SOPInstanceUID`. Be careful with this feature.", false)
1011         .SetAnswerField("ID", RestApiCallDocumentation::Type_String, "Orthanc identifier of the newly created instance")
1012         .SetAnswerField("Path", RestApiCallDocumentation::Type_String, "Path to access the instance in the REST API");
1013       return;
1014     }
1015 
1016     Json::Value request;
1017     if (!call.ParseJsonRequest(request) ||
1018         !request.isObject())
1019     {
1020       throw OrthancException(ErrorCode_BadRequest);
1021     }
1022 
1023     if (request.isMember(TAGS))
1024     {
1025       CreateDicomV2(call, request);
1026     }
1027     else
1028     {
1029       // Compatibility with Orthanc <= 0.9.3
1030       ParsedDicomFile dicom(true);
1031       CreateDicomV1(dicom, call, request);
1032 
1033       std::string id;
1034       StoreCreatedInstance(id, call, dicom, true);
1035     }
1036   }
1037 
1038 
SplitStudy(RestApiPostCall & call)1039   static void SplitStudy(RestApiPostCall& call)
1040   {
1041     if (call.IsDocumentation())
1042     {
1043       OrthancRestApi::DocumentSubmitCommandsJob(call);
1044       call.GetDocumentation()
1045         .SetTag("Studies")
1046         .SetSummary("Split study")
1047         .SetDescription("Start a new job so as to split the DICOM study whose Orthanc identifier is provided in the URL, "
1048                         "by taking some of its children series or instances out of it and putting them into a brand new study "
1049                         "(this new study is created by setting the `StudyInstanceUID` tag to a random identifier): "
1050                         "https://book.orthanc-server.com/users/anonymization.html#splitting")
1051         .SetUriArgument("id", "Orthanc identifier of the study of interest")
1052         .SetRequestField(SERIES, RestApiCallDocumentation::Type_JsonListOfStrings,
1053                          "The list of series to be separated from the parent study. "
1054                          "These series must all be children of the same source study, that is specified in the URI.", false)
1055         .SetRequestField(REPLACE, RestApiCallDocumentation::Type_JsonObject,
1056                          "Associative array to change the value of some DICOM tags in the new study. "
1057                          "These tags must be part of the \"Patient Module Attributes\" or the \"General Study "
1058                          "Module Attributes\", as specified by the DICOM 2011 standard in Tables C.7-1 and C.7-3.", false)
1059         .SetRequestField(REMOVE, RestApiCallDocumentation::Type_JsonListOfStrings,
1060                          "List of tags that must be removed in the new study (from the same modules as in the `Replace` option)", false)
1061         .SetRequestField(KEEP_SOURCE, RestApiCallDocumentation::Type_Boolean,
1062                          "If set to `true`, instructs Orthanc to keep a copy of the original series/instances in the source study. "
1063                          "By default, the original series/instances are deleted from Orthanc.", false)
1064         .SetRequestField(INSTANCES, RestApiCallDocumentation::Type_JsonListOfStrings,
1065                          "The list of instances to be separated from the parent study. "
1066                          "These instances must all be children of the same source study, that is specified in the URI.", false);
1067       return;
1068     }
1069 
1070     ServerContext& context = OrthancRestApi::GetContext(call);
1071 
1072     Json::Value request;
1073     if (!call.ParseJsonRequest(request))
1074     {
1075       // Bad JSON request
1076       throw OrthancException(ErrorCode_BadFileFormat);
1077     }
1078 
1079     const std::string study = call.GetUriComponent("id", "");
1080 
1081     std::unique_ptr<SplitStudyJob> job(new SplitStudyJob(context, study));
1082     job->SetOrigin(call);
1083 
1084     bool ok = false;
1085     if (request.isMember(SERIES))
1086     {
1087       std::vector<std::string> series;
1088       SerializationToolbox::ReadArrayOfStrings(series, request, SERIES);
1089 
1090       for (size_t i = 0; i < series.size(); i++)
1091       {
1092         job->AddSourceSeries(series[i]);
1093         ok = true;
1094       }
1095     }
1096 
1097     if (request.isMember(INSTANCES))
1098     {
1099       std::vector<std::string> instances;
1100       SerializationToolbox::ReadArrayOfStrings(instances, request, INSTANCES);
1101 
1102       for (size_t i = 0; i < instances.size(); i++)
1103       {
1104         job->AddSourceInstance(instances[i]);
1105         ok = true;
1106       }
1107     }
1108 
1109     if (!ok)
1110     {
1111       throw OrthancException(ErrorCode_BadRequest, "Both the \"Series\" and the \"Instances\" fields are missing");
1112     }
1113 
1114     job->AddTrailingStep();
1115 
1116     SetKeepSource(*job, request);
1117 
1118     if (request.isMember(REMOVE))
1119     {
1120       if (request[REMOVE].type() != Json::arrayValue)
1121       {
1122         throw OrthancException(ErrorCode_BadFileFormat);
1123       }
1124 
1125       for (Json::Value::ArrayIndex i = 0; i < request[REMOVE].size(); i++)
1126       {
1127         if (request[REMOVE][i].type() != Json::stringValue)
1128         {
1129           throw OrthancException(ErrorCode_BadFileFormat);
1130         }
1131         else
1132         {
1133           job->Remove(FromDcmtkBridge::ParseTag(request[REMOVE][i].asCString()));
1134         }
1135       }
1136     }
1137 
1138     if (request.isMember(REPLACE))
1139     {
1140       if (request[REPLACE].type() != Json::objectValue)
1141       {
1142         throw OrthancException(ErrorCode_BadFileFormat);
1143       }
1144 
1145       Json::Value::Members tags = request[REPLACE].getMemberNames();
1146 
1147       for (size_t i = 0; i < tags.size(); i++)
1148       {
1149         const Json::Value& value = request[REPLACE][tags[i]];
1150 
1151         if (value.type() != Json::stringValue)
1152         {
1153           throw OrthancException(ErrorCode_BadFileFormat);
1154         }
1155         else
1156         {
1157           job->Replace(FromDcmtkBridge::ParseTag(tags[i]), value.asString());
1158         }
1159       }
1160     }
1161 
1162     OrthancRestApi::GetApi(call).SubmitCommandsJob
1163       (call, job.release(), true /* synchronous by default */, request);
1164   }
1165 
1166 
MergeStudy(RestApiPostCall & call)1167   static void MergeStudy(RestApiPostCall& call)
1168   {
1169     if (call.IsDocumentation())
1170     {
1171       OrthancRestApi::DocumentSubmitCommandsJob(call);
1172       call.GetDocumentation()
1173         .SetTag("Studies")
1174         .SetSummary("Merge study")
1175         .SetDescription("Start a new job so as to move some DICOM resources into the DICOM study whose Orthanc identifier "
1176                         "is provided in the URL: https://book.orthanc-server.com/users/anonymization.html#merging")
1177         .SetUriArgument("id", "Orthanc identifier of the study of interest")
1178         .SetRequestField(RESOURCES, RestApiCallDocumentation::Type_JsonListOfStrings,
1179                          "The list of DICOM resources (studies, series, and/or instances) to be merged "
1180                          "into the study of interest (mandatory option)", true)
1181         .SetRequestField(KEEP_SOURCE, RestApiCallDocumentation::Type_Boolean,
1182                          "If set to `true`, instructs Orthanc to keep a copy of the original resources in their source study. "
1183                          "By default, the original resources are deleted from Orthanc.", false);
1184       return;
1185     }
1186 
1187     ServerContext& context = OrthancRestApi::GetContext(call);
1188 
1189     Json::Value request;
1190     if (!call.ParseJsonRequest(request))
1191     {
1192       // Bad JSON request
1193       throw OrthancException(ErrorCode_BadFileFormat);
1194     }
1195 
1196     const std::string study = call.GetUriComponent("id", "");
1197 
1198     std::unique_ptr<MergeStudyJob> job(new MergeStudyJob(context, study));
1199     job->SetOrigin(call);
1200 
1201     std::vector<std::string> resources;
1202     SerializationToolbox::ReadArrayOfStrings(resources, request, RESOURCES);
1203 
1204     for (size_t i = 0; i < resources.size(); i++)
1205     {
1206       job->AddSource(resources[i]);
1207     }
1208 
1209     job->AddTrailingStep();
1210 
1211     SetKeepSource(*job, request);
1212 
1213     OrthancRestApi::GetApi(call).SubmitCommandsJob
1214       (call, job.release(), true /* synchronous by default */, request);
1215   }
1216 
1217 
RegisterAnonymizeModify()1218   void OrthancRestApi::RegisterAnonymizeModify()
1219   {
1220     Register("/instances/{id}/modify", ModifyInstance);
1221     Register("/series/{id}/modify", ModifyResource<ResourceType_Series>);
1222     Register("/studies/{id}/modify", ModifyResource<ResourceType_Study>);
1223     Register("/patients/{id}/modify", ModifyResource<ResourceType_Patient>);
1224     Register("/tools/bulk-modify", BulkModify);
1225 
1226     Register("/instances/{id}/anonymize", AnonymizeInstance);
1227     Register("/series/{id}/anonymize", AnonymizeResource<ResourceType_Series>);
1228     Register("/studies/{id}/anonymize", AnonymizeResource<ResourceType_Study>);
1229     Register("/patients/{id}/anonymize", AnonymizeResource<ResourceType_Patient>);
1230     Register("/tools/bulk-anonymize", BulkAnonymize);
1231 
1232     Register("/tools/create-dicom", CreateDicom);
1233 
1234     Register("/studies/{id}/split", SplitStudy);
1235     Register("/studies/{id}/merge", MergeStudy);
1236   }
1237 }
1238