comparison OrthancServer/ServerJobs/MergeStudyJob.cpp @ 2853:52b017d22a4f

New URI: "/studies/.../merge" to merge a study
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 02 Oct 2018 17:05:07 +0200
parents
children 8b00e4cb4a6b
comparison
equal deleted inserted replaced
2851:859e880ac9a8 2853:52b017d22a4f
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-2018 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
40
41 namespace Orthanc
42 {
43 void MergeStudyJob::AddSourceSeriesInternal(const std::string& series)
44 {
45 // Generate a target SeriesInstanceUID for this series
46 seriesUidMap_[series] = FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Series);
47
48 // Add all the instances of the series as to be processed
49 std::list<std::string> instances;
50 context_.GetIndex().GetChildren(instances, series);
51
52 for (std::list<std::string>::const_iterator
53 it = instances.begin(); it != instances.end(); ++it)
54 {
55 AddInstance(*it);
56 }
57 }
58
59
60 void MergeStudyJob::AddSourceStudyInternal(const std::string& study)
61 {
62 if (study == targetStudy_)
63 {
64 LOG(ERROR) << "Cannot merge a study into the same study: " << study;
65 throw OrthancException(ErrorCode_UnknownResource);
66 }
67 else
68 {
69 std::list<std::string> series;
70 context_.GetIndex().GetChildren(series, study);
71
72 for (std::list<std::string>::const_iterator
73 it = series.begin(); it != series.end(); ++it)
74 {
75 AddSourceSeriesInternal(*it);
76 }
77 }
78 }
79
80
81 bool MergeStudyJob::HandleInstance(const std::string& instance)
82 {
83 /**
84 * Retrieve the DICOM instance to be modified
85 **/
86
87 std::auto_ptr<ParsedDicomFile> modified;
88
89 try
90 {
91 ServerContext::DicomCacheLocker locker(context_, instance);
92 modified.reset(locker.GetDicom().Clone(true));
93 }
94 catch (OrthancException&)
95 {
96 LOG(WARNING) << "An instance was removed after the job was issued: " << instance;
97 return false;
98 }
99
100
101 /**
102 * Chose the target UIDs
103 **/
104
105 std::string series = modified->GetHasher().HashSeries();
106
107 SeriesUidMap::const_iterator targetSeriesUid = seriesUidMap_.find(series);
108
109 if (targetSeriesUid == seriesUidMap_.end())
110 {
111 throw OrthancException(ErrorCode_BadFileFormat); // Should never happen
112 }
113
114
115 /**
116 * Copy the tags from the "Patient Module Attributes" and "General
117 * Study Module Attributes" modules of the target study
118 **/
119
120 for (std::set<DicomTag>::const_iterator it = removals_.begin();
121 it != removals_.end(); ++it)
122 {
123 modified->Remove(*it);
124 }
125
126 for (Replacements::const_iterator it = replacements_.begin();
127 it != replacements_.end(); ++it)
128 {
129 modified->ReplacePlainString(it->first, it->second);
130 }
131
132
133 /**
134 * Store the new instance into Orthanc
135 **/
136
137 modified->ReplacePlainString(DICOM_TAG_SERIES_INSTANCE_UID, targetSeriesUid->second);
138
139 DicomInstanceToStore toStore;
140 toStore.SetOrigin(origin_);
141 toStore.SetParsedDicomFile(*modified);
142
143 std::string modifiedInstance;
144 if (context_.Store(modifiedInstance, toStore) != StoreStatus_Success)
145 {
146 LOG(ERROR) << "Error while storing a modified instance " << instance;
147 return false;
148 }
149
150 return true;
151 }
152
153
154 bool MergeStudyJob::HandleTrailingStep()
155 {
156 if (!keepSource_)
157 {
158 const size_t n = GetInstancesCount();
159
160 for (size_t i = 0; i < n; i++)
161 {
162 Json::Value tmp;
163 context_.DeleteResource(tmp, GetInstance(i), ResourceType_Instance);
164 }
165 }
166
167 return true;
168 }
169
170
171 MergeStudyJob::MergeStudyJob(ServerContext& context,
172 const std::string& targetStudy) :
173 SetOfInstancesJob(true /* with trailing step */),
174 context_(context),
175 keepSource_(false),
176 targetStudy_(targetStudy)
177 {
178 /**
179 * Check the validity of the input ID
180 **/
181
182 ResourceType type;
183
184 if (!context_.GetIndex().LookupResourceType(type, targetStudy) ||
185 type != ResourceType_Study)
186 {
187 LOG(ERROR) << "Cannot merge into an unknown study: " << targetStudy;
188 throw OrthancException(ErrorCode_UnknownResource);
189 }
190
191
192 /**
193 * Detect the tags to be removed/replaced by parsing one child
194 * instance of the study
195 **/
196
197 DicomTag::AddTagsForModule(removals_, DicomModule_Patient);
198 DicomTag::AddTagsForModule(removals_, DicomModule_Study);
199
200 std::list<std::string> instances;
201 context_.GetIndex().GetChildInstances(instances, targetStudy);
202
203 if (instances.empty())
204 {
205 throw OrthancException(ErrorCode_UnknownResource);
206 }
207
208 DicomMap dicom;
209
210 {
211 ServerContext::DicomCacheLocker locker(context_, instances.front());
212 locker.GetDicom().ExtractDicomSummary(dicom);
213 }
214
215 const std::set<DicomTag> moduleTags = removals_;
216 for (std::set<DicomTag>::const_iterator it = moduleTags.begin();
217 it != moduleTags.end(); ++it)
218 {
219 const DicomValue* value = dicom.TestAndGetValue(*it);
220 std::string str;
221
222 if (value != NULL &&
223 value->CopyToString(str, false))
224 {
225 removals_.erase(*it);
226 replacements_.insert(std::make_pair(*it, str));
227 }
228 }
229 }
230
231
232 void MergeStudyJob::SetOrigin(const DicomInstanceOrigin& origin)
233 {
234 if (IsStarted())
235 {
236 throw OrthancException(ErrorCode_BadSequenceOfCalls);
237 }
238 else
239 {
240 origin_ = origin;
241 }
242 }
243
244
245 void MergeStudyJob::SetOrigin(const RestApiCall& call)
246 {
247 SetOrigin(DicomInstanceOrigin::FromRest(call));
248 }
249
250
251 void MergeStudyJob::AddSource(const std::string& studyOrSeries)
252 {
253 ResourceType level;
254
255 if (IsStarted())
256 {
257 throw OrthancException(ErrorCode_BadSequenceOfCalls);
258 }
259 else if (!context_.GetIndex().LookupResourceType(level, studyOrSeries))
260 {
261 LOG(ERROR) << "Cannot find this resource: " << studyOrSeries;
262 throw OrthancException(ErrorCode_UnknownResource);
263 }
264 else
265 {
266 switch (level)
267 {
268 case ResourceType_Study:
269 AddSourceStudyInternal(studyOrSeries);
270 break;
271
272 case ResourceType_Series:
273 AddSourceSeries(studyOrSeries);
274 break;
275
276 default:
277 LOG(ERROR) << "This resource is neither a study, nor a series: "
278 << studyOrSeries << " is a " << EnumerationToString(level);
279 throw OrthancException(ErrorCode_UnknownResource);
280 }
281 }
282 }
283
284
285 void MergeStudyJob::AddSourceSeries(const std::string& series)
286 {
287 std::string parent;
288
289 if (IsStarted())
290 {
291 throw OrthancException(ErrorCode_BadSequenceOfCalls);
292 }
293 else if (!context_.GetIndex().LookupParent(parent, series, ResourceType_Study))
294 {
295 LOG(ERROR) << "This resource is not a series: " << series;
296 throw OrthancException(ErrorCode_UnknownResource);
297 }
298 else if (parent == targetStudy_)
299 {
300 LOG(ERROR) << "Cannot merge series " << series
301 << " into its parent study " << targetStudy_;
302 throw OrthancException(ErrorCode_UnknownResource);
303 }
304 else
305 {
306 AddSourceSeriesInternal(series);
307 }
308 }
309
310
311 void MergeStudyJob::AddSourceStudy(const std::string& study)
312 {
313 ResourceType actualLevel;
314
315 if (IsStarted())
316 {
317 throw OrthancException(ErrorCode_BadSequenceOfCalls);
318 }
319 else if (!context_.GetIndex().LookupResourceType(actualLevel, study) ||
320 actualLevel != ResourceType_Study)
321 {
322 LOG(ERROR) << "This resource is not a study: " << study;
323 throw OrthancException(ErrorCode_UnknownResource);
324 }
325 else
326 {
327 AddSourceStudyInternal(study);
328 }
329 }
330
331
332 void MergeStudyJob::SetKeepSource(bool keep)
333 {
334 if (IsStarted())
335 {
336 throw OrthancException(ErrorCode_BadSequenceOfCalls);
337 }
338
339 keepSource_ = keep;
340 }
341
342
343 void MergeStudyJob::GetPublicContent(Json::Value& value)
344 {
345 SetOfInstancesJob::GetPublicContent(value);
346 value["TargetStudy"] = targetStudy_;
347 }
348
349
350 static const char* KEEP_SOURCE = "KeepSource";
351 static const char* TARGET_STUDY = "TargetStudy";
352 static const char* REPLACEMENTS = "Replacements";
353 static const char* REMOVALS = "Removals";
354 static const char* SERIES_UID_MAP = "SeriesUIDMap";
355 static const char* ORIGIN = "Origin";
356
357
358 MergeStudyJob::MergeStudyJob(ServerContext& context,
359 const Json::Value& serialized) :
360 SetOfInstancesJob(serialized), // (*)
361 context_(context)
362 {
363 if (!HasTrailingStep())
364 {
365 // Should have been set by (*)
366 throw OrthancException(ErrorCode_InternalError);
367 }
368
369 keepSource_ = SerializationToolbox::ReadBoolean(serialized, KEEP_SOURCE);
370 targetStudy_ = SerializationToolbox::ReadString(serialized, TARGET_STUDY);
371 SerializationToolbox::ReadMapOfTags(replacements_, serialized, REPLACEMENTS);
372 SerializationToolbox::ReadSetOfTags(removals_, serialized, REMOVALS);
373 SerializationToolbox::ReadMapOfStrings(seriesUidMap_, serialized, SERIES_UID_MAP);
374 origin_ = DicomInstanceOrigin(serialized[ORIGIN]);
375 }
376
377
378 bool MergeStudyJob::Serialize(Json::Value& target)
379 {
380 if (!SetOfInstancesJob::Serialize(target))
381 {
382 return false;
383 }
384 else
385 {
386 target[KEEP_SOURCE] = keepSource_;
387 target[TARGET_STUDY] = targetStudy_;
388 SerializationToolbox::WriteMapOfTags(target, replacements_, REPLACEMENTS);
389 SerializationToolbox::WriteSetOfTags(target, removals_, REMOVALS);
390 SerializationToolbox::WriteMapOfStrings(target, seriesUidMap_, SERIES_UID_MAP);
391 origin_.Serialize(target[ORIGIN]);
392
393 return true;
394 }
395 }
396 }