comparison OrthancServer/OrthancFindRequestHandler.cpp @ 1364:111e23bb4904 query-retrieve

integration mainline->query-retrieve
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 21 May 2015 16:58:30 +0200
parents c2c28dd17e87 94ffb597d297
children 5c11c4e728eb
comparison
equal deleted inserted replaced
953:f894be6e7cc1 1364:111e23bb4904
1 /** 1 /**
2 * Orthanc - A Lightweight, RESTful DICOM Store 2 * Orthanc - A Lightweight, RESTful DICOM Store
3 * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, 3 * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
4 * Belgium 4 * Department, University Hospital of Liege, Belgium
5 * 5 *
6 * This program is free software: you can redistribute it and/or 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 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 8 * published by the Free Software Foundation, either version 3 of the
9 * License, or (at your option) any later version. 9 * License, or (at your option) any later version.
39 #include "../Core/DicomFormat/DicomArray.h" 39 #include "../Core/DicomFormat/DicomArray.h"
40 #include "ServerToolbox.h" 40 #include "ServerToolbox.h"
41 #include "OrthancInitialization.h" 41 #include "OrthancInitialization.h"
42 #include "FromDcmtkBridge.h" 42 #include "FromDcmtkBridge.h"
43 43
44 #include "ResourceFinder.h"
45 #include "DicomFindQuery.h"
46
47
44 namespace Orthanc 48 namespace Orthanc
45 { 49 {
46 static bool IsWildcard(const std::string& constraint)
47 {
48 return (constraint.find('-') != std::string::npos ||
49 constraint.find('*') != std::string::npos ||
50 constraint.find('\\') != std::string::npos ||
51 constraint.find('?') != std::string::npos);
52 }
53
54 static bool ApplyRangeConstraint(const std::string& value,
55 const std::string& constraint)
56 {
57 size_t separator = constraint.find('-');
58 std::string lower, upper, v;
59 Toolbox::ToLowerCase(lower, constraint.substr(0, separator));
60 Toolbox::ToLowerCase(upper, constraint.substr(separator + 1));
61 Toolbox::ToLowerCase(v, value);
62
63 if (lower.size() == 0 && upper.size() == 0)
64 {
65 return false;
66 }
67
68 if (lower.size() == 0)
69 {
70 return v <= upper;
71 }
72
73 if (upper.size() == 0)
74 {
75 return v >= lower;
76 }
77
78 return (v >= lower && v <= upper);
79 }
80
81
82 static bool ApplyListConstraint(const std::string& value,
83 const std::string& constraint)
84 {
85 std::string v1;
86 Toolbox::ToLowerCase(v1, value);
87
88 std::vector<std::string> items;
89 Toolbox::TokenizeString(items, constraint, '\\');
90
91 for (size_t i = 0; i < items.size(); i++)
92 {
93 std::string lower;
94 Toolbox::ToLowerCase(lower, items[i]);
95 if (lower == v1)
96 {
97 return true;
98 }
99 }
100
101 return false;
102 }
103
104
105 static bool Matches(const std::string& value,
106 const std::string& constraint)
107 {
108 // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained
109 // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html
110
111 if (constraint.find('-') != std::string::npos)
112 {
113 return ApplyRangeConstraint(value, constraint);
114 }
115
116 if (constraint.find('\\') != std::string::npos)
117 {
118 return ApplyListConstraint(value, constraint);
119 }
120
121 if (constraint.find('*') != std::string::npos ||
122 constraint.find('?') != std::string::npos)
123 {
124 // TODO - Cache the constructed regular expression
125 boost::regex pattern(Toolbox::WildcardToRegularExpression(constraint),
126 boost::regex::icase /* case insensitive search */);
127 return boost::regex_match(value, pattern);
128 }
129 else
130 {
131 std::string v, c;
132 Toolbox::ToLowerCase(v, value);
133 Toolbox::ToLowerCase(c, constraint);
134 return v == c;
135 }
136 }
137
138
139 static bool LookupOneInstance(std::string& result,
140 ServerIndex& index,
141 const std::string& id,
142 ResourceType type)
143 {
144 if (type == ResourceType_Instance)
145 {
146 result = id;
147 return true;
148 }
149
150 std::string childId;
151
152 {
153 std::list<std::string> children;
154 index.GetChildInstances(children, id);
155
156 if (children.empty())
157 {
158 return false;
159 }
160
161 childId = children.front();
162 }
163
164 return LookupOneInstance(result, index, childId, GetChildResourceType(type));
165 }
166
167
168 static bool Matches(const Json::Value& resource,
169 const DicomArray& query)
170 {
171 for (size_t i = 0; i < query.GetSize(); i++)
172 {
173 if (query.GetElement(i).GetValue().IsNull() ||
174 query.GetElement(i).GetTag() == DICOM_TAG_QUERY_RETRIEVE_LEVEL ||
175 query.GetElement(i).GetTag() == DICOM_TAG_SPECIFIC_CHARACTER_SET ||
176 query.GetElement(i).GetTag() == DICOM_TAG_MODALITIES_IN_STUDY)
177 {
178 continue;
179 }
180
181 std::string tag = query.GetElement(i).GetTag().Format();
182 std::string value;
183 if (resource.isMember(tag))
184 {
185 value = resource.get(tag, Json::arrayValue).get("Value", "").asString();
186 }
187
188 if (!Matches(value, query.GetElement(i).GetValue().AsString()))
189 {
190 return false;
191 }
192 }
193
194 return true;
195 }
196
197
198 static void AddAnswer(DicomFindAnswers& answers, 50 static void AddAnswer(DicomFindAnswers& answers,
199 const Json::Value& resource, 51 const Json::Value& resource,
200 const DicomArray& query) 52 const DicomArray& query)
201 { 53 {
202 DicomMap result; 54 DicomMap result;
203 55
204 for (size_t i = 0; i < query.GetSize(); i++) 56 for (size_t i = 0; i < query.GetSize(); i++)
205 { 57 {
206 if (query.GetElement(i).GetTag() != DICOM_TAG_QUERY_RETRIEVE_LEVEL && 58 // Fix issue 30 (QR response missing "Query/Retrieve Level" (008,0052))
207 query.GetElement(i).GetTag() != DICOM_TAG_SPECIFIC_CHARACTER_SET) 59 if (query.GetElement(i).GetTag() == DICOM_TAG_QUERY_RETRIEVE_LEVEL)
60 {
61 result.SetValue(query.GetElement(i).GetTag(), query.GetElement(i).GetValue());
62 }
63 else if (query.GetElement(i).GetTag() == DICOM_TAG_SPECIFIC_CHARACTER_SET)
64 {
65 }
66 else
208 { 67 {
209 std::string tag = query.GetElement(i).GetTag().Format(); 68 std::string tag = query.GetElement(i).GetTag().Format();
210 std::string value; 69 std::string value;
211 if (resource.isMember(tag)) 70 if (resource.isMember(tag))
212 { 71 {
218 result.SetValue(query.GetElement(i).GetTag(), ""); 77 result.SetValue(query.GetElement(i).GetTag(), "");
219 } 78 }
220 } 79 }
221 } 80 }
222 81
223 answers.Add(result); 82 if (result.GetSize() == 0)
83 {
84 LOG(WARNING) << "The C-FIND request does not return any DICOM tag";
85 }
86 else
87 {
88 answers.Add(result);
89 }
224 } 90 }
225 91
226 92
227 static bool ApplyModalitiesInStudyFilter(std::list<std::string>& filteredStudies, 93 namespace
228 const std::list<std::string>& studies,
229 const DicomMap& input,
230 ServerIndex& index)
231 { 94 {
232 filteredStudies.clear(); 95 class CFindQuery : public DicomFindQuery
233 96 {
234 const DicomValue& v = input.GetValue(DICOM_TAG_MODALITIES_IN_STUDY); 97 private:
235 if (v.IsNull()) 98 DicomFindAnswers& answers_;
236 { 99 ServerIndex& index_;
237 return false; 100 const DicomArray& query_;
238 } 101 bool hasModalitiesInStudy_;
239 102 std::set<std::string> modalitiesInStudy_;
240 // Move the allowed modalities into a "std::set" 103
241 std::vector<std::string> tmp; 104 public:
242 Toolbox::TokenizeString(tmp, v.AsString(), '\\'); 105 CFindQuery(DicomFindAnswers& answers,
243 106 ServerIndex& index,
244 std::set<std::string> modalities; 107 const DicomArray& query) :
245 for (size_t i = 0; i < tmp.size(); i++) 108 answers_(answers),
246 { 109 index_(index),
247 modalities.insert(tmp[i]); 110 query_(query),
248 } 111 hasModalitiesInStudy_(false)
249 112 {
250 // Loop over the studies 113 }
251 for (std::list<std::string>::const_iterator 114
252 it = studies.begin(); it != studies.end(); ++it) 115 void SetModalitiesInStudy(const std::string& value)
253 { 116 {
254 try 117 hasModalitiesInStudy_ = true;
255 { 118
256 // We are considering a single study. Check whether one of 119 std::vector<std::string> tmp;
257 // its child series matches one of the modalities. 120 Toolbox::TokenizeString(tmp, value, '\\');
258 Json::Value study; 121
259 if (index.LookupResource(study, *it, ResourceType_Study)) 122 for (size_t i = 0; i < tmp.size(); i++)
260 { 123 {
261 // Loop over the series of the considered study. 124 modalitiesInStudy_.insert(tmp[i]);
262 for (Json::Value::ArrayIndex j = 0; j < study["Series"].size(); j++) // (*) 125 }
126 }
127
128 virtual bool HasMainDicomTagsFilter(ResourceType level) const
129 {
130 if (DicomFindQuery::HasMainDicomTagsFilter(level))
131 {
132 return true;
133 }
134
135 return (level == ResourceType_Study &&
136 hasModalitiesInStudy_);
137 }
138
139 virtual bool FilterMainDicomTags(const std::string& resourceId,
140 ResourceType level,
141 const DicomMap& mainTags) const
142 {
143 if (!DicomFindQuery::FilterMainDicomTags(resourceId, level, mainTags))
144 {
145 return false;
146 }
147
148 if (level != ResourceType_Study ||
149 !hasModalitiesInStudy_)
150 {
151 return true;
152 }
153
154 try
155 {
156 // We are considering a single study, and the
157 // "MODALITIES_IN_STUDY" tag is set in the C-Find. Check
158 // whether one of its child series matches one of the
159 // modalities.
160
161 Json::Value study;
162 if (index_.LookupResource(study, resourceId, ResourceType_Study))
263 { 163 {
264 Json::Value series; 164 // Loop over the series of the considered study.
265 if (index.LookupResource(series, study["Series"][j].asString(), ResourceType_Series)) 165 for (Json::Value::ArrayIndex j = 0; j < study["Series"].size(); j++)
266 { 166 {
267 // Get the modality of this series 167 Json::Value series;
268 if (series["MainDicomTags"].isMember("Modality")) 168 if (index_.LookupResource(series, study["Series"][j].asString(), ResourceType_Series))
269 { 169 {
270 std::string modality = series["MainDicomTags"]["Modality"].asString(); 170 // Get the modality of this series
271 if (modalities.find(modality) != modalities.end()) 171 if (series["MainDicomTags"].isMember("Modality"))
272 { 172 {
273 // This series of the considered study matches one 173 std::string modality = series["MainDicomTags"]["Modality"].asString();
274 // of the required modalities. Take the study into 174 if (modalitiesInStudy_.find(modality) != modalitiesInStudy_.end())
275 // consideration for future filtering. 175 {
276 filteredStudies.push_back(*it); 176 // This series of the considered study matches one
277 177 // of the required modalities. Take the study into
278 // We have finished considering this study. Break the study loop at (*). 178 // consideration for future filtering.
279 break; 179 return true;
180 }
280 } 181 }
281 } 182 }
282 } 183 }
283 } 184 }
284 } 185 }
285 } 186 catch (OrthancException&)
286 catch (OrthancException&) 187 {
287 { 188 // This resource has probably been deleted during the find request
288 // This resource has probably been deleted during the find request 189 }
289 } 190
290 } 191 return false;
291 192 }
292 return true; 193
293 } 194 virtual bool HasInstanceFilter() const
294 195 {
295 196 return true;
296 namespace 197 }
297 { 198
298 class CandidateResources 199 virtual bool FilterInstance(const std::string& instanceId,
299 { 200 const Json::Value& content) const
300 private: 201 {
301 ServerIndex& index_; 202 bool ok = DicomFindQuery::FilterInstance(instanceId, content);
302 ModalityManufacturer manufacturer_; 203
303 ResourceType level_; 204 if (ok)
304 bool isFilterApplied_; 205 {
305 std::set<std::string> filtered_; 206 // Add this resource to the answers
306 207 AddAnswer(answers_, content, query_);
307 static void ListToSet(std::set<std::string>& target, 208 }
308 const std::list<std::string>& source) 209
309 { 210 return ok;
310 for (std::list<std::string>::const_iterator
311 it = source.begin(); it != source.end(); ++it)
312 {
313 target.insert(*it);
314 }
315 }
316
317 void ApplyExactFilter(const DicomTag& tag, const std::string& value)
318 {
319 LOG(INFO) << "Applying exact filter on tag "
320 << FromDcmtkBridge::GetName(tag) << " (value: " << value << ")";
321
322 std::list<std::string> resources;
323 index_.LookupTagValue(resources, tag, value, level_);
324
325 if (isFilterApplied_)
326 {
327 std::set<std::string> s;
328 ListToSet(s, resources);
329
330 std::set<std::string> tmp = filtered_;
331 filtered_.clear();
332
333 for (std::set<std::string>::const_iterator
334 it = tmp.begin(); it != tmp.end(); ++it)
335 {
336 if (s.find(*it) != s.end())
337 {
338 filtered_.insert(*it);
339 }
340 }
341 }
342 else
343 {
344 assert(filtered_.empty());
345 isFilterApplied_ = true;
346 ListToSet(filtered_, resources);
347 }
348 }
349
350 public:
351 CandidateResources(ServerIndex& index,
352 ModalityManufacturer manufacturer) :
353 index_(index),
354 manufacturer_(manufacturer),
355 level_(ResourceType_Patient),
356 isFilterApplied_(false)
357 {
358 }
359
360 ResourceType GetLevel() const
361 {
362 return level_;
363 }
364
365 void GoDown()
366 {
367 assert(level_ != ResourceType_Instance);
368
369 if (isFilterApplied_)
370 {
371 std::set<std::string> tmp = filtered_;
372
373 filtered_.clear();
374
375 for (std::set<std::string>::const_iterator
376 it = tmp.begin(); it != tmp.end(); ++it)
377 {
378 std::list<std::string> children;
379 index_.GetChildren(children, *it);
380 ListToSet(filtered_, children);
381 }
382 }
383
384 switch (level_)
385 {
386 case ResourceType_Patient:
387 level_ = ResourceType_Study;
388 break;
389
390 case ResourceType_Study:
391 level_ = ResourceType_Series;
392 break;
393
394 case ResourceType_Series:
395 level_ = ResourceType_Instance;
396 break;
397
398 default:
399 throw OrthancException(ErrorCode_InternalError);
400 }
401 }
402
403 void Flatten(std::list<std::string>& resources) const
404 {
405 resources.clear();
406
407 if (isFilterApplied_)
408 {
409 for (std::set<std::string>::const_iterator
410 it = filtered_.begin(); it != filtered_.end(); ++it)
411 {
412 resources.push_back(*it);
413 }
414 }
415 else
416 {
417 Json::Value tmp;
418 index_.GetAllUuids(tmp, level_);
419 for (Json::Value::ArrayIndex i = 0; i < tmp.size(); i++)
420 {
421 resources.push_back(tmp[i].asString());
422 }
423 }
424 }
425
426 void ApplyFilter(const DicomTag& tag, const DicomMap& query)
427 {
428 if (query.HasTag(tag))
429 {
430 const DicomValue& value = query.GetValue(tag);
431 if (!value.IsNull())
432 {
433 std::string value = query.GetValue(tag).AsString();
434 if (!IsWildcard(value))
435 {
436 ApplyExactFilter(tag, value);
437 }
438 }
439 }
440 } 211 }
441 }; 212 };
442 } 213 }
443 214
444
445 bool OrthancFindRequestHandler::HasReachedLimit(const DicomFindAnswers& answers,
446 ResourceType level) const
447 {
448 switch (level)
449 {
450 case ResourceType_Patient:
451 case ResourceType_Study:
452 case ResourceType_Series:
453 return (maxResults_ != 0 && answers.GetSize() >= maxResults_);
454
455 case ResourceType_Instance:
456 return (maxInstances_ != 0 && answers.GetSize() >= maxInstances_);
457
458 default:
459 throw OrthancException(ErrorCode_InternalError);
460 }
461 }
462 215
463 216
464 bool OrthancFindRequestHandler::Handle(DicomFindAnswers& answers, 217 bool OrthancFindRequestHandler::Handle(DicomFindAnswers& answers,
465 const DicomMap& input, 218 const DicomMap& input,
466 const std::string& callingAETitle) 219 const std::string& callingAETitle)
467 { 220 {
468 /** 221 /**
469 * Retrieve the manufacturer of this modality. 222 * Ensure that the calling modality is known to Orthanc.
470 **/ 223 **/
471 224
472 ModalityManufacturer manufacturer; 225 RemoteModalityParameters modality;
473 226
474 { 227 if (!Configuration::LookupDicomModalityUsingAETitle(modality, callingAETitle))
475 RemoteModalityParameters modality; 228 {
476 229 throw OrthancException("Unknown modality");
477 if (!Configuration::LookupDicomModalityUsingAETitle(modality, callingAETitle)) 230 }
478 { 231
479 throw OrthancException("Unknown modality"); 232 // ModalityManufacturer manufacturer = modality.GetManufacturer();
480 }
481
482 manufacturer = modality.GetManufacturer();
483 }
484 233
485 234
486 /** 235 /**
487 * Retrieve the query level. 236 * Retrieve the query level.
488 **/ 237 **/
517 } 266 }
518 } 267 }
519 268
520 269
521 /** 270 /**
522 * Retrieve the candidate resources for this query level. Whenever 271 * Build up the query object.
523 * possible, we avoid returning ALL the resources for this query
524 * level, as it would imply reading the JSON file on the harddisk
525 * for each of them.
526 **/ 272 **/
527 273
528 CandidateResources candidates(context_.GetIndex(), manufacturer); 274 CFindQuery findQuery(answers, context_.GetIndex(), query);
529 275 findQuery.SetLevel(level);
530 for (;;) 276
531 { 277 for (size_t i = 0; i < query.GetSize(); i++)
532 switch (candidates.GetLevel()) 278 {
533 { 279 const DicomTag tag = query.GetElement(i).GetTag();
534 case ResourceType_Patient: 280
535 candidates.ApplyFilter(DICOM_TAG_PATIENT_ID, input); 281 if (query.GetElement(i).GetValue().IsNull() ||
536 break; 282 tag == DICOM_TAG_QUERY_RETRIEVE_LEVEL ||
537 283 tag == DICOM_TAG_SPECIFIC_CHARACTER_SET)
538 case ResourceType_Study: 284 {
539 candidates.ApplyFilter(DICOM_TAG_STUDY_INSTANCE_UID, input); 285 continue;
540 candidates.ApplyFilter(DICOM_TAG_ACCESSION_NUMBER, input); 286 }
541 break; 287
542 288 std::string value = query.GetElement(i).GetValue().AsString();
543 case ResourceType_Series: 289
544 candidates.ApplyFilter(DICOM_TAG_SERIES_INSTANCE_UID, input); 290 if (tag == DICOM_TAG_MODALITIES_IN_STUDY)
545 break; 291 {
546 292 findQuery.SetModalitiesInStudy(value);
547 case ResourceType_Instance: 293 }
548 candidates.ApplyFilter(DICOM_TAG_SOP_INSTANCE_UID, input); 294 else
549 break; 295 {
550 296 findQuery.SetConstraint(tag, value);
551 default: 297 }
552 throw OrthancException(ErrorCode_InternalError); 298 }
553 } 299
554 300
555 if (candidates.GetLevel() == level) 301 /**
556 { 302 * Run the query.
303 **/
304
305 ResourceFinder finder(context_);
306
307 switch (level)
308 {
309 case ResourceType_Patient:
310 case ResourceType_Study:
311 case ResourceType_Series:
312 finder.SetMaxResults(maxResults_);
557 break; 313 break;
558 } 314
559 315 case ResourceType_Instance:
560 candidates.GoDown(); 316 finder.SetMaxResults(maxInstances_);
561 } 317 break;
562 318
563 std::list<std::string> resources; 319 default:
564 candidates.Flatten(resources); 320 throw OrthancException(ErrorCode_InternalError);
565 321 }
566 LOG(INFO) << "Number of candidate resources after exact filtering: " << resources.size(); 322
567 323 std::list<std::string> tmp;
568 /** 324 bool finished = finder.Apply(tmp, findQuery);
569 * Apply filtering on modalities for studies, if asked (this is an 325
570 * extension to standard DICOM) 326 LOG(INFO) << "Number of matching resources: " << tmp.size();
571 * http://www.medicalconnections.co.uk/kb/Filtering_on_and_Retrieving_the_Modality_in_a_C_FIND 327
572 **/ 328 return finished;
573
574 if (level == ResourceType_Study &&
575 input.HasTag(DICOM_TAG_MODALITIES_IN_STUDY))
576 {
577 std::list<std::string> filtered;
578 if (ApplyModalitiesInStudyFilter(filtered, resources, input, context_.GetIndex()))
579 {
580 resources = filtered;
581 }
582 }
583
584
585 /**
586 * Loop over all the resources for this query level.
587 **/
588
589 for (std::list<std::string>::const_iterator
590 resource = resources.begin(); resource != resources.end(); ++resource)
591 {
592 try
593 {
594 std::string instance;
595 if (LookupOneInstance(instance, context_.GetIndex(), *resource, level))
596 {
597 Json::Value info;
598 context_.ReadJson(info, instance);
599
600 if (Matches(info, query))
601 {
602 if (HasReachedLimit(answers, level))
603 {
604 // Too many results, stop before recording this new match
605 return false;
606 }
607
608 AddAnswer(answers, info, query);
609 }
610 }
611 }
612 catch (OrthancException&)
613 {
614 // This resource has probably been deleted during the find request
615 }
616 }
617
618 return true; // All the matching resources have been returned
619 } 329 }
620 } 330 }
621
622
623
624 /**
625 * TODO : Case-insensitive match for PN value representation (Patient
626 * Name). Case-senstive match for all the other value representations.
627 *
628 * Reference: DICOM PS 3.4
629 * - C.2.2.2.1 ("Single Value Matching")
630 * - C.2.2.2.4 ("Wild Card Matching")
631 * http://medical.nema.org/Dicom/2011/11_04pu.pdf (
632 **/