Mercurial > hg > orthanc
comparison OrthancServer/OrthancFindRequestHandler.cpp @ 624:b58d65608949
integration find-move-scp -> mainline
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Fri, 25 Oct 2013 12:42:38 +0200 |
parents | 5ab377df6d8b |
children | 08eca5d86aad |
comparison
equal
deleted
inserted
replaced
609:5651d2c6f6fd | 624:b58d65608949 |
---|---|
1 /** | |
2 * Orthanc - A Lightweight, RESTful DICOM Store | |
3 * Copyright (C) 2012-2013 Medical Physics Department, CHU of Liege, | |
4 * Belgium | |
5 * | |
6 * This program is free software: you can redistribute it and/or | |
7 * modify it under the terms of the GNU General Public License as | |
8 * published by the Free Software Foundation, either version 3 of the | |
9 * License, or (at your option) any later version. | |
10 * | |
11 * In addition, as a special exception, the copyright holders of this | |
12 * program give permission to link the code of its release with the | |
13 * OpenSSL project's "OpenSSL" library (or with modified versions of it | |
14 * that use the same license as the "OpenSSL" library), and distribute | |
15 * the linked executables. You must obey the GNU General Public License | |
16 * in all respects for all of the code used other than "OpenSSL". If you | |
17 * modify file(s) with this exception, you may extend this exception to | |
18 * your version of the file(s), but you are not obligated to do so. If | |
19 * you do not wish to do so, delete this exception statement from your | |
20 * version. If you delete this exception statement from all source files | |
21 * in the program, then also delete it here. | |
22 * | |
23 * This program is distributed in the hope that it will be useful, but | |
24 * WITHOUT ANY WARRANTY; without even the implied warranty of | |
25 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
26 * General Public License for more details. | |
27 * | |
28 * You should have received a copy of the GNU General Public License | |
29 * along with this program. If not, see <http://www.gnu.org/licenses/>. | |
30 **/ | |
31 | |
32 #include "OrthancFindRequestHandler.h" | |
33 | |
34 #include <glog/logging.h> | |
35 #include <boost/regex.hpp> | |
36 | |
37 #include "../Core/DicomFormat/DicomArray.h" | |
38 #include "ServerToolbox.h" | |
39 #include "OrthancInitialization.h" | |
40 | |
41 namespace Orthanc | |
42 { | |
43 static bool IsWildcard(const std::string& constraint) | |
44 { | |
45 return (constraint.find('-') != std::string::npos || | |
46 constraint.find('*') != std::string::npos || | |
47 constraint.find('\\') != std::string::npos || | |
48 constraint.find('?') != std::string::npos); | |
49 } | |
50 | |
51 static std::string ToLowerCase(const std::string& s) | |
52 { | |
53 std::string result = s; | |
54 Toolbox::ToLowerCase(result); | |
55 return result; | |
56 } | |
57 | |
58 static bool ApplyRangeConstraint(const std::string& value, | |
59 const std::string& constraint) | |
60 { | |
61 size_t separator = constraint.find('-'); | |
62 std::string lower = ToLowerCase(constraint.substr(0, separator)); | |
63 std::string upper = ToLowerCase(constraint.substr(separator + 1)); | |
64 std::string v = ToLowerCase(value); | |
65 | |
66 if (lower.size() == 0 && upper.size() == 0) | |
67 { | |
68 return false; | |
69 } | |
70 | |
71 if (lower.size() == 0) | |
72 { | |
73 return v <= upper; | |
74 } | |
75 | |
76 if (upper.size() == 0) | |
77 { | |
78 return v >= lower; | |
79 } | |
80 | |
81 return (v >= lower && v <= upper); | |
82 } | |
83 | |
84 | |
85 static bool ApplyListConstraint(const std::string& value, | |
86 const std::string& constraint) | |
87 { | |
88 std::string v1 = ToLowerCase(value); | |
89 | |
90 std::vector<std::string> items; | |
91 Toolbox::TokenizeString(items, constraint, '\\'); | |
92 | |
93 for (size_t i = 0; i < items.size(); i++) | |
94 { | |
95 Toolbox::ToLowerCase(items[i]); | |
96 if (items[i] == v1) | |
97 { | |
98 return true; | |
99 } | |
100 } | |
101 | |
102 return false; | |
103 } | |
104 | |
105 | |
106 static bool Matches(const std::string& value, | |
107 const std::string& constraint) | |
108 { | |
109 // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained | |
110 // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html | |
111 | |
112 if (constraint.find('-') != std::string::npos) | |
113 { | |
114 return ApplyRangeConstraint(value, constraint); | |
115 } | |
116 | |
117 if (constraint.find('\\') != std::string::npos) | |
118 { | |
119 return ApplyListConstraint(value, constraint); | |
120 } | |
121 | |
122 if (constraint.find('*') != std::string::npos || | |
123 constraint.find('?') != std::string::npos) | |
124 { | |
125 // TODO - Cache the constructed regular expression | |
126 boost::regex pattern(Toolbox::WildcardToRegularExpression(constraint), | |
127 boost::regex::icase /* case insensitive search */); | |
128 return boost::regex_match(value, pattern); | |
129 } | |
130 else | |
131 { | |
132 return ToLowerCase(value) == ToLowerCase(constraint); | |
133 } | |
134 } | |
135 | |
136 | |
137 static bool LookupOneInstance(std::string& result, | |
138 ServerIndex& index, | |
139 const std::string& id, | |
140 ResourceType type) | |
141 { | |
142 if (type == ResourceType_Instance) | |
143 { | |
144 result = id; | |
145 return true; | |
146 } | |
147 | |
148 std::string childId; | |
149 | |
150 { | |
151 std::list<std::string> children; | |
152 index.GetChildInstances(children, id); | |
153 | |
154 if (children.size() == 0) | |
155 { | |
156 return false; | |
157 } | |
158 | |
159 childId = children.front(); | |
160 } | |
161 | |
162 return LookupOneInstance(result, index, childId, GetChildResourceType(type)); | |
163 } | |
164 | |
165 | |
166 static bool Matches(const Json::Value& resource, | |
167 const DicomArray& query) | |
168 { | |
169 for (size_t i = 0; i < query.GetSize(); i++) | |
170 { | |
171 if (query.GetElement(i).GetValue().IsNull() || | |
172 query.GetElement(i).GetTag() == DICOM_TAG_QUERY_RETRIEVE_LEVEL || | |
173 query.GetElement(i).GetTag() == DICOM_TAG_SPECIFIC_CHARACTER_SET || | |
174 query.GetElement(i).GetTag() == DICOM_TAG_MODALITIES_IN_STUDY) | |
175 { | |
176 continue; | |
177 } | |
178 | |
179 std::string tag = query.GetElement(i).GetTag().Format(); | |
180 std::string value; | |
181 if (resource.isMember(tag)) | |
182 { | |
183 value = resource.get(tag, Json::arrayValue).get("Value", "").asString(); | |
184 } | |
185 | |
186 if (!Matches(value, query.GetElement(i).GetValue().AsString())) | |
187 { | |
188 return false; | |
189 } | |
190 } | |
191 | |
192 return true; | |
193 } | |
194 | |
195 | |
196 static void AddAnswer(DicomFindAnswers& answers, | |
197 const Json::Value& resource, | |
198 const DicomArray& query) | |
199 { | |
200 DicomMap result; | |
201 | |
202 for (size_t i = 0; i < query.GetSize(); i++) | |
203 { | |
204 if (query.GetElement(i).GetTag() != DICOM_TAG_QUERY_RETRIEVE_LEVEL && | |
205 query.GetElement(i).GetTag() != DICOM_TAG_SPECIFIC_CHARACTER_SET) | |
206 { | |
207 std::string tag = query.GetElement(i).GetTag().Format(); | |
208 std::string value; | |
209 if (resource.isMember(tag)) | |
210 { | |
211 value = resource.get(tag, Json::arrayValue).get("Value", "").asString(); | |
212 result.SetValue(query.GetElement(i).GetTag(), value); | |
213 } | |
214 } | |
215 } | |
216 | |
217 answers.Add(result); | |
218 } | |
219 | |
220 | |
221 static bool ApplyModalitiesInStudyFilter(std::list<std::string>& filteredStudies, | |
222 const std::list<std::string>& studies, | |
223 const DicomMap& input, | |
224 ServerIndex& index) | |
225 { | |
226 filteredStudies.clear(); | |
227 | |
228 const DicomValue& v = input.GetValue(DICOM_TAG_MODALITIES_IN_STUDY); | |
229 if (v.IsNull()) | |
230 { | |
231 return false; | |
232 } | |
233 | |
234 // Move the allowed modalities into a "std::set" | |
235 std::vector<std::string> tmp; | |
236 Toolbox::TokenizeString(tmp, v.AsString(), '\\'); | |
237 | |
238 std::set<std::string> modalities; | |
239 for (size_t i = 0; i < tmp.size(); i++) | |
240 { | |
241 modalities.insert(tmp[i]); | |
242 } | |
243 | |
244 // Loop over the studies | |
245 for (std::list<std::string>::const_iterator | |
246 it = studies.begin(); it != studies.end(); it++) | |
247 { | |
248 try | |
249 { | |
250 // We are considering a single study. Check whether one of | |
251 // its child series matches one of the modalities. | |
252 Json::Value study; | |
253 if (index.LookupResource(study, *it, ResourceType_Study)) | |
254 { | |
255 // Loop over the series of the considered study. | |
256 for (Json::Value::ArrayIndex j = 0; j < study["Series"].size(); j++) // (*) | |
257 { | |
258 Json::Value series; | |
259 if (index.LookupResource(series, study["Series"][j].asString(), ResourceType_Series)) | |
260 { | |
261 // Get the modality of this series | |
262 if (series["MainDicomTags"].isMember("Modality")) | |
263 { | |
264 std::string modality = series["MainDicomTags"]["Modality"].asString(); | |
265 if (modalities.find(modality) != modalities.end()) | |
266 { | |
267 // This series of the considered study matches one | |
268 // of the required modalities. Take the study into | |
269 // consideration for future filtering. | |
270 filteredStudies.push_back(*it); | |
271 | |
272 // We have finished considering this study. Break the study loop at (*). | |
273 break; | |
274 } | |
275 } | |
276 } | |
277 } | |
278 } | |
279 } | |
280 catch (OrthancException&) | |
281 { | |
282 // This resource has probably been deleted during the find request | |
283 } | |
284 } | |
285 | |
286 return true; | |
287 } | |
288 | |
289 | |
290 static bool LookupCandidateResourcesInternal(/* out */ std::list<std::string>& resources, | |
291 /* in */ ServerIndex& index, | |
292 /* in */ ResourceType level, | |
293 /* in */ const DicomMap& query, | |
294 /* in */ DicomTag tag) | |
295 { | |
296 if (query.HasTag(tag)) | |
297 { | |
298 const DicomValue& value = query.GetValue(tag); | |
299 if (!value.IsNull()) | |
300 { | |
301 std::string str = query.GetValue(tag).AsString(); | |
302 if (!IsWildcard(str)) | |
303 { | |
304 index.LookupTagValue(resources, tag, str/*, level*/); | |
305 return true; | |
306 } | |
307 } | |
308 } | |
309 | |
310 return false; | |
311 } | |
312 | |
313 | |
314 static void LookupCandidateResources(/* out */ std::list<std::string>& resources, | |
315 /* in */ ServerIndex& index, | |
316 /* in */ ResourceType level, | |
317 /* in */ const DicomMap& query) | |
318 { | |
319 // TODO : Speed up using full querying against the MainDicomTags. | |
320 | |
321 resources.clear(); | |
322 | |
323 bool done = false; | |
324 | |
325 switch (level) | |
326 { | |
327 case ResourceType_Patient: | |
328 done = LookupCandidateResourcesInternal(resources, index, level, query, DICOM_TAG_PATIENT_ID); | |
329 break; | |
330 | |
331 case ResourceType_Study: | |
332 done = LookupCandidateResourcesInternal(resources, index, level, query, DICOM_TAG_STUDY_INSTANCE_UID); | |
333 break; | |
334 | |
335 case ResourceType_Series: | |
336 done = LookupCandidateResourcesInternal(resources, index, level, query, DICOM_TAG_SERIES_INSTANCE_UID); | |
337 break; | |
338 | |
339 case ResourceType_Instance: | |
340 done = LookupCandidateResourcesInternal(resources, index, level, query, DICOM_TAG_SOP_INSTANCE_UID); | |
341 break; | |
342 | |
343 default: | |
344 break; | |
345 } | |
346 | |
347 if (!done) | |
348 { | |
349 Json::Value allResources; | |
350 index.GetAllUuids(allResources, level); | |
351 assert(allResources.type() == Json::arrayValue); | |
352 | |
353 for (Json::Value::ArrayIndex i = 0; i < allResources.size(); i++) | |
354 { | |
355 resources.push_back(allResources[i].asString()); | |
356 } | |
357 } | |
358 } | |
359 | |
360 | |
361 void OrthancFindRequestHandler::Handle(DicomFindAnswers& answers, | |
362 const DicomMap& input) | |
363 { | |
364 /** | |
365 * Retrieve the query level. | |
366 **/ | |
367 | |
368 const DicomValue* levelTmp = input.TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL); | |
369 if (levelTmp == NULL) | |
370 { | |
371 throw OrthancException(ErrorCode_BadRequest); | |
372 } | |
373 | |
374 ResourceType level = StringToResourceType(levelTmp->AsString().c_str()); | |
375 | |
376 if (level != ResourceType_Patient && | |
377 level != ResourceType_Study && | |
378 level != ResourceType_Series) | |
379 { | |
380 throw OrthancException(ErrorCode_NotImplemented); | |
381 } | |
382 | |
383 | |
384 /** | |
385 * Retrieve the candidate resources for this query level. Whenever | |
386 * possible, we avoid returning ALL the resources for this query | |
387 * level, as it would imply reading the JSON file on the harddisk | |
388 * for each of them. | |
389 **/ | |
390 | |
391 std::list<std::string> resources; | |
392 LookupCandidateResources(resources, context_.GetIndex(), level, input); | |
393 | |
394 | |
395 /** | |
396 * Apply filtering on modalities for studies, if asked (this is an | |
397 * extension to standard DICOM) | |
398 * http://www.medicalconnections.co.uk/kb/Filtering_on_and_Retrieving_the_Modality_in_a_C_FIND | |
399 **/ | |
400 | |
401 if (level == ResourceType_Study && | |
402 input.HasTag(DICOM_TAG_MODALITIES_IN_STUDY)) | |
403 { | |
404 std::list<std::string> filtered; | |
405 if (ApplyModalitiesInStudyFilter(filtered, resources, input, context_.GetIndex())) | |
406 { | |
407 resources = filtered; | |
408 } | |
409 } | |
410 | |
411 | |
412 /** | |
413 * Loop over all the resources for this query level. | |
414 **/ | |
415 | |
416 DicomArray query(input); | |
417 for (std::list<std::string>::const_iterator | |
418 resource = resources.begin(); resource != resources.end(); resource++) | |
419 { | |
420 try | |
421 { | |
422 std::string instance; | |
423 if (LookupOneInstance(instance, context_.GetIndex(), *resource, level)) | |
424 { | |
425 Json::Value info; | |
426 context_.ReadJson(info, instance); | |
427 | |
428 if (Matches(info, query)) | |
429 { | |
430 AddAnswer(answers, info, query); | |
431 } | |
432 } | |
433 } | |
434 catch (OrthancException&) | |
435 { | |
436 // This resource has probably been deleted during the find request | |
437 } | |
438 } | |
439 } | |
440 } |