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