comparison OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp @ 4044:d25f4c0fa160 framework

splitting code into OrthancFramework and OrthancServer
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 10 Jun 2020 20:30:34 +0200
parents OrthancServer/ServerJobs/MergeStudyJob.cpp@771dbd9eb3bd
children 05b8fd21089c
comparison
equal deleted inserted replaced
4043:6c6239aec462 4044:d25f4c0fa160
1 /**
2 * Orthanc - A Lightweight, RESTful DICOM Store
3 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
4 * Department, University Hospital of Liege, Belgium
5 * Copyright (C) 2017-2020 Osimis S.A., Belgium
6 *
7 * This program is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License as
9 * published by the Free Software Foundation, either version 3 of the
10 * License, or (at your option) any later version.
11 *
12 * In addition, as a special exception, the copyright holders of this
13 * program give permission to link the code of its release with the
14 * OpenSSL project's "OpenSSL" library (or with modified versions of it
15 * that use the same license as the "OpenSSL" library), and distribute
16 * the linked executables. You must obey the GNU General Public License
17 * in all respects for all of the code used other than "OpenSSL". If you
18 * modify file(s) with this exception, you may extend this exception to
19 * your version of the file(s), but you are not obligated to do so. If
20 * you do not wish to do so, delete this exception statement from your
21 * version. If you delete this exception statement from all source files
22 * in the program, then also delete it here.
23 *
24 * This program is distributed in the hope that it will be useful, but
25 * WITHOUT ANY WARRANTY; without even the implied warranty of
26 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
27 * General Public License for more details.
28 *
29 * You should have received a copy of the GNU General Public License
30 * along with this program. If not, see <http://www.gnu.org/licenses/>.
31 **/
32
33
34 #include "MergeStudyJob.h"
35
36 #include "../../Core/DicomParsing/FromDcmtkBridge.h"
37 #include "../../Core/Logging.h"
38 #include "../../Core/SerializationToolbox.h"
39 #include "../ServerContext.h"
40
41
42 namespace Orthanc
43 {
44 void MergeStudyJob::AddSourceSeriesInternal(const std::string& series)
45 {
46 // Generate a target SeriesInstanceUID for this series
47 seriesUidMap_[series] = FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Series);
48
49 // Add all the instances of the series as to be processed
50 std::list<std::string> instances;
51 GetContext().GetIndex().GetChildren(instances, series);
52
53 for (std::list<std::string>::const_iterator
54 it = instances.begin(); it != instances.end(); ++it)
55 {
56 AddInstance(*it);
57 }
58 }
59
60
61 void MergeStudyJob::AddSourceStudyInternal(const std::string& study)
62 {
63 if (study == targetStudy_)
64 {
65 throw OrthancException(ErrorCode_UnknownResource,
66 "Cannot merge a study into the same study: " + study);
67 }
68 else
69 {
70 std::list<std::string> series;
71 GetContext().GetIndex().GetChildren(series, study);
72
73 for (std::list<std::string>::const_iterator
74 it = series.begin(); it != series.end(); ++it)
75 {
76 AddSourceSeriesInternal(*it);
77 }
78 }
79 }
80
81
82 bool MergeStudyJob::HandleInstance(const std::string& instance)
83 {
84 if (!HasTrailingStep())
85 {
86 throw OrthancException(ErrorCode_BadSequenceOfCalls,
87 "AddTrailingStep() should have been called after AddSourceXXX()");
88 }
89
90 /**
91 * Retrieve the DICOM instance to be modified
92 **/
93
94 std::unique_ptr<ParsedDicomFile> modified;
95
96 try
97 {
98 ServerContext::DicomCacheLocker locker(GetContext(), instance);
99 modified.reset(locker.GetDicom().Clone(true));
100 }
101 catch (OrthancException&)
102 {
103 LOG(WARNING) << "An instance was removed after the job was issued: " << instance;
104 return false;
105 }
106
107
108 /**
109 * Chose the target UIDs
110 **/
111
112 std::string series = modified->GetHasher().HashSeries();
113
114 SeriesUidMap::const_iterator targetSeriesUid = seriesUidMap_.find(series);
115
116 if (targetSeriesUid == seriesUidMap_.end())
117 {
118 throw OrthancException(ErrorCode_BadFileFormat); // Should never happen
119 }
120
121
122 /**
123 * Copy the tags from the "Patient Module Attributes" and "General
124 * Study Module Attributes" modules of the target study
125 **/
126
127 for (std::set<DicomTag>::const_iterator it = removals_.begin();
128 it != removals_.end(); ++it)
129 {
130 modified->Remove(*it);
131 }
132
133 for (Replacements::const_iterator it = replacements_.begin();
134 it != replacements_.end(); ++it)
135 {
136 modified->ReplacePlainString(it->first, it->second);
137 }
138
139
140 /**
141 * Store the new instance into Orthanc
142 **/
143
144 modified->ReplacePlainString(DICOM_TAG_SERIES_INSTANCE_UID, targetSeriesUid->second);
145
146 // Fix since Orthanc 1.5.8: Assign new "SOPInstanceUID", as the instance has been modified
147 modified->ReplacePlainString(DICOM_TAG_SOP_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Instance));
148
149 DicomInstanceToStore toStore;
150 toStore.SetOrigin(origin_);
151 toStore.SetParsedDicomFile(*modified);
152
153 std::string modifiedInstance;
154 if (GetContext().Store(modifiedInstance, toStore,
155 StoreInstanceMode_Default) != StoreStatus_Success)
156 {
157 LOG(ERROR) << "Error while storing a modified instance " << instance;
158 return false;
159 }
160
161 return true;
162 }
163
164
165 MergeStudyJob::MergeStudyJob(ServerContext& context,
166 const std::string& targetStudy) :
167 CleaningInstancesJob(context, false /* by default, remove source instances */),
168 targetStudy_(targetStudy)
169 {
170 /**
171 * Check the validity of the input ID
172 **/
173
174 ResourceType type;
175
176 if (!GetContext().GetIndex().LookupResourceType(type, targetStudy) ||
177 type != ResourceType_Study)
178 {
179 throw OrthancException(ErrorCode_UnknownResource,
180 "Cannot merge into an unknown study: " + targetStudy);
181 }
182
183
184 /**
185 * Detect the tags to be removed/replaced by parsing one child
186 * instance of the study
187 **/
188
189 DicomTag::AddTagsForModule(removals_, DicomModule_Patient);
190 DicomTag::AddTagsForModule(removals_, DicomModule_Study);
191
192 std::list<std::string> instances;
193 GetContext().GetIndex().GetChildInstances(instances, targetStudy);
194
195 if (instances.empty())
196 {
197 throw OrthancException(ErrorCode_UnknownResource);
198 }
199
200 DicomMap dicom;
201
202 {
203 ServerContext::DicomCacheLocker locker(GetContext(), instances.front());
204 locker.GetDicom().ExtractDicomSummary(dicom);
205 }
206
207 const std::set<DicomTag> moduleTags = removals_;
208 for (std::set<DicomTag>::const_iterator it = moduleTags.begin();
209 it != moduleTags.end(); ++it)
210 {
211 const DicomValue* value = dicom.TestAndGetValue(*it);
212 std::string str;
213
214 if (value != NULL &&
215 value->CopyToString(str, false))
216 {
217 removals_.erase(*it);
218 replacements_.insert(std::make_pair(*it, str));
219 }
220 }
221 }
222
223
224 void MergeStudyJob::SetOrigin(const DicomInstanceOrigin& origin)
225 {
226 if (IsStarted())
227 {
228 throw OrthancException(ErrorCode_BadSequenceOfCalls);
229 }
230 else
231 {
232 origin_ = origin;
233 }
234 }
235
236
237 void MergeStudyJob::SetOrigin(const RestApiCall& call)
238 {
239 SetOrigin(DicomInstanceOrigin::FromRest(call));
240 }
241
242
243 void MergeStudyJob::AddSource(const std::string& studyOrSeries)
244 {
245 ResourceType level;
246
247 if (IsStarted())
248 {
249 throw OrthancException(ErrorCode_BadSequenceOfCalls);
250 }
251 else if (!GetContext().GetIndex().LookupResourceType(level, studyOrSeries))
252 {
253 throw OrthancException(ErrorCode_UnknownResource,
254 "Cannot find this resource: " + studyOrSeries);
255 }
256 else
257 {
258 switch (level)
259 {
260 case ResourceType_Study:
261 AddSourceStudyInternal(studyOrSeries);
262 break;
263
264 case ResourceType_Series:
265 AddSourceSeries(studyOrSeries);
266 break;
267
268 default:
269 throw OrthancException(ErrorCode_UnknownResource,
270 "This resource is neither a study, nor a series: " +
271 studyOrSeries + " is a " +
272 std::string(EnumerationToString(level)));
273 }
274 }
275 }
276
277
278 void MergeStudyJob::AddSourceSeries(const std::string& series)
279 {
280 std::string parent;
281
282 if (IsStarted())
283 {
284 throw OrthancException(ErrorCode_BadSequenceOfCalls);
285 }
286 else if (!GetContext().GetIndex().LookupParent(parent, series, ResourceType_Study))
287 {
288 throw OrthancException(ErrorCode_UnknownResource,
289 "This resource is not a series: " + series);
290 }
291 else if (parent == targetStudy_)
292 {
293 throw OrthancException(ErrorCode_UnknownResource,
294 "Cannot merge series " + series +
295 " into its parent study " + targetStudy_);
296 }
297 else
298 {
299 AddSourceSeriesInternal(series);
300 }
301 }
302
303
304 void MergeStudyJob::AddSourceStudy(const std::string& study)
305 {
306 ResourceType actualLevel;
307
308 if (IsStarted())
309 {
310 throw OrthancException(ErrorCode_BadSequenceOfCalls);
311 }
312 else if (!GetContext().GetIndex().LookupResourceType(actualLevel, study) ||
313 actualLevel != ResourceType_Study)
314 {
315 throw OrthancException(ErrorCode_UnknownResource,
316 "This resource is not a study: " + study);
317 }
318 else
319 {
320 AddSourceStudyInternal(study);
321 }
322 }
323
324
325 void MergeStudyJob::GetPublicContent(Json::Value& value)
326 {
327 CleaningInstancesJob::GetPublicContent(value);
328 value["TargetStudy"] = targetStudy_;
329 }
330
331
332 static const char* TARGET_STUDY = "TargetStudy";
333 static const char* REPLACEMENTS = "Replacements";
334 static const char* REMOVALS = "Removals";
335 static const char* SERIES_UID_MAP = "SeriesUIDMap";
336 static const char* ORIGIN = "Origin";
337
338
339 MergeStudyJob::MergeStudyJob(ServerContext& context,
340 const Json::Value& serialized) :
341 CleaningInstancesJob(context, serialized,
342 false /* by default, remove source instances */) // (*)
343 {
344 if (!HasTrailingStep())
345 {
346 // Should have been set by (*)
347 throw OrthancException(ErrorCode_InternalError);
348 }
349
350 targetStudy_ = SerializationToolbox::ReadString(serialized, TARGET_STUDY);
351 SerializationToolbox::ReadMapOfTags(replacements_, serialized, REPLACEMENTS);
352 SerializationToolbox::ReadSetOfTags(removals_, serialized, REMOVALS);
353 SerializationToolbox::ReadMapOfStrings(seriesUidMap_, serialized, SERIES_UID_MAP);
354 origin_ = DicomInstanceOrigin(serialized[ORIGIN]);
355 }
356
357
358 bool MergeStudyJob::Serialize(Json::Value& target)
359 {
360 if (!CleaningInstancesJob::Serialize(target))
361 {
362 return false;
363 }
364 else
365 {
366 target[TARGET_STUDY] = targetStudy_;
367 SerializationToolbox::WriteMapOfTags(target, replacements_, REPLACEMENTS);
368 SerializationToolbox::WriteSetOfTags(target, removals_, REMOVALS);
369 SerializationToolbox::WriteMapOfStrings(target, seriesUidMap_, SERIES_UID_MAP);
370 origin_.Serialize(target[ORIGIN]);
371
372 return true;
373 }
374 }
375 }