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 }