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