comparison OrthancServer/OrthancFindRequestHandler.cpp @ 1361:94ffb597d297

refactoring of C-Find SCP
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 15 May 2015 17:19:33 +0200
parents 3dd494f201a1
children 111e23bb4904 a1745d9be6e9
comparison
equal deleted inserted replaced
1360:0649c5aef34a 1361:94ffb597d297
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 bool isFirst)
202 { 53 {
203 DicomMap result; 54 DicomMap result;
204 55
205 for (size_t i = 0; i < query.GetSize(); i++) 56 for (size_t i = 0; i < query.GetSize(); i++)
206 { 57 {
228 } 79 }
229 } 80 }
230 81
231 if (result.GetSize() == 0) 82 if (result.GetSize() == 0)
232 { 83 {
233 if (isFirst) 84 LOG(WARNING) << "The C-FIND request does not return any DICOM tag";
234 {
235 LOG(WARNING) << "The C-FIND request does not return any DICOM tag";
236 }
237 } 85 }
238 else 86 else
239 { 87 {
240 answers.Add(result); 88 answers.Add(result);
241 } 89 }
242 } 90 }
243 91
244 92
245 static bool ApplyModalitiesInStudyFilter(std::list<std::string>& filteredStudies, 93 namespace
246 const std::list<std::string>& studies,
247 const DicomMap& input,
248 ServerIndex& index)
249 { 94 {
250 filteredStudies.clear(); 95 class CFindQuery : public DicomFindQuery
251 96 {
252 const DicomValue& v = input.GetValue(DICOM_TAG_MODALITIES_IN_STUDY); 97 private:
253 if (v.IsNull()) 98 DicomFindAnswers& answers_;
254 { 99 ServerIndex& index_;
255 return false; 100 const DicomArray& query_;
256 } 101 bool hasModalitiesInStudy_;
257 102 std::set<std::string> modalitiesInStudy_;
258 // Move the allowed modalities into a "std::set" 103
259 std::vector<std::string> tmp; 104 public:
260 Toolbox::TokenizeString(tmp, v.AsString(), '\\'); 105 CFindQuery(DicomFindAnswers& answers,
261 106 ServerIndex& index,
262 std::set<std::string> modalities; 107 const DicomArray& query) :
263 for (size_t i = 0; i < tmp.size(); i++) 108 answers_(answers),
264 { 109 index_(index),
265 modalities.insert(tmp[i]); 110 query_(query),
266 } 111 hasModalitiesInStudy_(false)
267 112 {
268 // Loop over the studies 113 }
269 for (std::list<std::string>::const_iterator 114
270 it = studies.begin(); it != studies.end(); ++it) 115 void SetModalitiesInStudy(const std::string& value)
271 { 116 {
272 try 117 hasModalitiesInStudy_ = true;
273 { 118
274 // We are considering a single study. Check whether one of 119 std::vector<std::string> tmp;
275 // its child series matches one of the modalities. 120 Toolbox::TokenizeString(tmp, value, '\\');
276 Json::Value study; 121
277 if (index.LookupResource(study, *it, ResourceType_Study)) 122 for (size_t i = 0; i < tmp.size(); i++)
278 { 123 {
279 // Loop over the series of the considered study. 124 modalitiesInStudy_.insert(tmp[i]);
280 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))
281 { 163 {
282 Json::Value series; 164 // Loop over the series of the considered study.
283 if (index.LookupResource(series, study["Series"][j].asString(), ResourceType_Series)) 165 for (Json::Value::ArrayIndex j = 0; j < study["Series"].size(); j++)
284 { 166 {
285 // Get the modality of this series 167 Json::Value series;
286 if (series["MainDicomTags"].isMember("Modality")) 168 if (index_.LookupResource(series, study["Series"][j].asString(), ResourceType_Series))
287 { 169 {
288 std::string modality = series["MainDicomTags"]["Modality"].asString(); 170 // Get the modality of this series
289 if (modalities.find(modality) != modalities.end()) 171 if (series["MainDicomTags"].isMember("Modality"))
290 { 172 {
291 // This series of the considered study matches one 173 std::string modality = series["MainDicomTags"]["Modality"].asString();
292 // of the required modalities. Take the study into 174 if (modalitiesInStudy_.find(modality) != modalitiesInStudy_.end())
293 // consideration for future filtering. 175 {
294 filteredStudies.push_back(*it); 176 // This series of the considered study matches one
295 177 // of the required modalities. Take the study into
296 // We have finished considering this study. Break the study loop at (*). 178 // consideration for future filtering.
297 break; 179 return true;
180 }
298 } 181 }
299 } 182 }
300 } 183 }
301 } 184 }
302 } 185 }
303 } 186 catch (OrthancException&)
304 catch (OrthancException&) 187 {
305 { 188 // This resource has probably been deleted during the find request
306 // This resource has probably been deleted during the find request 189 }
307 } 190
308 } 191 return false;
309 192 }
310 return true; 193
311 } 194 virtual bool HasInstanceFilter() const
312 195 {
313 196 return true;
314 namespace 197 }
315 { 198
316 class CandidateResources 199 virtual bool FilterInstance(const std::string& instanceId,
317 { 200 const Json::Value& content) const
318 private: 201 {
319 ServerIndex& index_; 202 bool ok = DicomFindQuery::FilterInstance(instanceId, content);
320 ModalityManufacturer manufacturer_; 203
321 ResourceType level_; 204 if (ok)
322 bool isFilterApplied_; 205 {
323 std::set<std::string> filtered_; 206 // Add this resource to the answers
324 207 AddAnswer(answers_, content, query_);
325 static void ListToSet(std::set<std::string>& target, 208 }
326 const std::list<std::string>& source) 209
327 { 210 return ok;
328 for (std::list<std::string>::const_iterator
329 it = source.begin(); it != source.end(); ++it)
330 {
331 target.insert(*it);
332 }
333 }
334
335 void ApplyExactFilter(const DicomTag& tag, const std::string& value)
336 {
337 LOG(INFO) << "Applying exact filter on tag "
338 << FromDcmtkBridge::GetName(tag) << " (value: " << value << ")";
339
340 std::list<std::string> resources;
341 index_.LookupIdentifier(resources, tag, value, level_);
342
343 if (isFilterApplied_)
344 {
345 std::set<std::string> s;
346 ListToSet(s, resources);
347
348 std::set<std::string> tmp = filtered_;
349 filtered_.clear();
350
351 for (std::set<std::string>::const_iterator
352 it = tmp.begin(); it != tmp.end(); ++it)
353 {
354 if (s.find(*it) != s.end())
355 {
356 filtered_.insert(*it);
357 }
358 }
359 }
360 else
361 {
362 assert(filtered_.empty());
363 isFilterApplied_ = true;
364 ListToSet(filtered_, resources);
365 }
366 }
367
368 public:
369 CandidateResources(ServerIndex& index,
370 ModalityManufacturer manufacturer) :
371 index_(index),
372 manufacturer_(manufacturer),
373 level_(ResourceType_Patient),
374 isFilterApplied_(false)
375 {
376 }
377
378 ResourceType GetLevel() const
379 {
380 return level_;
381 }
382
383 void GoDown()
384 {
385 assert(level_ != ResourceType_Instance);
386
387 if (isFilterApplied_)
388 {
389 std::set<std::string> tmp = filtered_;
390
391 filtered_.clear();
392
393 for (std::set<std::string>::const_iterator
394 it = tmp.begin(); it != tmp.end(); ++it)
395 {
396 std::list<std::string> children;
397 index_.GetChildren(children, *it);
398 ListToSet(filtered_, children);
399 }
400 }
401
402 switch (level_)
403 {
404 case ResourceType_Patient:
405 level_ = ResourceType_Study;
406 break;
407
408 case ResourceType_Study:
409 level_ = ResourceType_Series;
410 break;
411
412 case ResourceType_Series:
413 level_ = ResourceType_Instance;
414 break;
415
416 default:
417 throw OrthancException(ErrorCode_InternalError);
418 }
419 }
420
421 void Flatten(std::list<std::string>& resources) const
422 {
423 resources.clear();
424
425 if (isFilterApplied_)
426 {
427 for (std::set<std::string>::const_iterator
428 it = filtered_.begin(); it != filtered_.end(); ++it)
429 {
430 resources.push_back(*it);
431 }
432 }
433 else
434 {
435 index_.GetAllUuids(resources, level_);
436 }
437 }
438
439 void ApplyFilter(const DicomTag& tag, const DicomMap& query)
440 {
441 if (query.HasTag(tag))
442 {
443 const DicomValue& value = query.GetValue(tag);
444 if (!value.IsNull())
445 {
446 std::string value = query.GetValue(tag).AsString();
447 if (!IsWildcard(value))
448 {
449 ApplyExactFilter(tag, value);
450 }
451 }
452 }
453 } 211 }
454 }; 212 };
455 } 213 }
456 214
457
458 bool OrthancFindRequestHandler::HasReachedLimit(const DicomFindAnswers& answers,
459 ResourceType level) const
460 {
461 switch (level)
462 {
463 case ResourceType_Patient:
464 case ResourceType_Study:
465 case ResourceType_Series:
466 return (maxResults_ != 0 && answers.GetSize() >= maxResults_);
467
468 case ResourceType_Instance:
469 return (maxInstances_ != 0 && answers.GetSize() >= maxInstances_);
470
471 default:
472 throw OrthancException(ErrorCode_InternalError);
473 }
474 }
475 215
476 216
477 bool OrthancFindRequestHandler::Handle(DicomFindAnswers& answers, 217 bool OrthancFindRequestHandler::Handle(DicomFindAnswers& answers,
478 const DicomMap& input, 218 const DicomMap& input,
479 const std::string& callingAETitle) 219 const std::string& callingAETitle)
480 { 220 {
481 /** 221 /**
482 * Retrieve the manufacturer of this modality. 222 * Ensure that the calling modality is known to Orthanc.
483 **/ 223 **/
484 224
485 ModalityManufacturer manufacturer; 225 RemoteModalityParameters modality;
486 226
487 { 227 if (!Configuration::LookupDicomModalityUsingAETitle(modality, callingAETitle))
488 RemoteModalityParameters modality; 228 {
489 229 throw OrthancException("Unknown modality");
490 if (!Configuration::LookupDicomModalityUsingAETitle(modality, callingAETitle)) 230 }
491 { 231
492 throw OrthancException("Unknown modality"); 232 // ModalityManufacturer manufacturer = modality.GetManufacturer();
493 }
494
495 manufacturer = modality.GetManufacturer();
496 }
497 233
498 234
499 /** 235 /**
500 * Retrieve the query level. 236 * Retrieve the query level.
501 **/ 237 **/
530 } 266 }
531 } 267 }
532 268
533 269
534 /** 270 /**
535 * Retrieve the candidate resources for this query level. Whenever 271 * Build up the query object.
536 * possible, we avoid returning ALL the resources for this query
537 * level, as it would imply reading the JSON file on the harddisk
538 * for each of them.
539 **/ 272 **/
540 273
541 CandidateResources candidates(context_.GetIndex(), manufacturer); 274 CFindQuery findQuery(answers, context_.GetIndex(), query);
542 275 findQuery.SetLevel(level);
543 for (;;) 276
544 { 277 for (size_t i = 0; i < query.GetSize(); i++)
545 switch (candidates.GetLevel()) 278 {
546 { 279 const DicomTag tag = query.GetElement(i).GetTag();
547 case ResourceType_Patient: 280
548 candidates.ApplyFilter(DICOM_TAG_PATIENT_ID, input); 281 if (query.GetElement(i).GetValue().IsNull() ||
549 break; 282 tag == DICOM_TAG_QUERY_RETRIEVE_LEVEL ||
550 283 tag == DICOM_TAG_SPECIFIC_CHARACTER_SET)
551 case ResourceType_Study: 284 {
552 candidates.ApplyFilter(DICOM_TAG_STUDY_INSTANCE_UID, input); 285 continue;
553 candidates.ApplyFilter(DICOM_TAG_ACCESSION_NUMBER, input); 286 }
554 break; 287
555 288 std::string value = query.GetElement(i).GetValue().AsString();
556 case ResourceType_Series: 289
557 candidates.ApplyFilter(DICOM_TAG_SERIES_INSTANCE_UID, input); 290 if (tag == DICOM_TAG_MODALITIES_IN_STUDY)
558 break; 291 {
559 292 findQuery.SetModalitiesInStudy(value);
560 case ResourceType_Instance: 293 }
561 candidates.ApplyFilter(DICOM_TAG_SOP_INSTANCE_UID, input); 294 else
562 break; 295 {
563 296 findQuery.SetConstraint(tag, value);
564 default: 297 }
565 throw OrthancException(ErrorCode_InternalError); 298 }
566 } 299
567 300
568 if (candidates.GetLevel() == level) 301 /**
569 { 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_);
570 break; 313 break;
571 } 314
572 315 case ResourceType_Instance:
573 candidates.GoDown(); 316 finder.SetMaxResults(maxInstances_);
574 } 317 break;
575 318
576 std::list<std::string> resources; 319 default:
577 candidates.Flatten(resources); 320 throw OrthancException(ErrorCode_InternalError);
578 321 }
579 322
580 /** 323 std::list<std::string> tmp;
581 * Apply filtering on modalities for studies, if asked (this is an 324 bool finished = finder.Apply(tmp, findQuery);
582 * extension to standard DICOM) 325
583 * http://www.medicalconnections.co.uk/kb/Filtering_on_and_Retrieving_the_Modality_in_a_C_FIND 326 LOG(INFO) << "Number of matching resources: " << tmp.size();
584 **/ 327
585 328 return finished;
586 if (level == ResourceType_Study &&
587 input.HasTag(DICOM_TAG_MODALITIES_IN_STUDY))
588 {
589 std::list<std::string> filtered;
590 if (ApplyModalitiesInStudyFilter(filtered, resources, input, context_.GetIndex()))
591 {
592 resources = filtered;
593 }
594 }
595
596 /**
597 * Loop over all the resources for this query level.
598 **/
599
600 LOG(INFO) << "Number of candidate resources after exact filtering on the identifiers only: " << resources.size();
601
602 bool isFirst = true;
603
604 for (std::list<std::string>::const_iterator
605 resource = resources.begin(); resource != resources.end(); ++resource)
606 {
607 try
608 {
609 std::string instance;
610 if (LookupOneInstance(instance, context_.GetIndex(), *resource, level))
611 {
612 Json::Value info;
613 context_.ReadJson(info, instance);
614
615 if (Matches(info, query))
616 {
617 if (HasReachedLimit(answers, level))
618 {
619 // Too many results, stop before recording this new match
620 return false;
621 }
622
623 AddAnswer(answers, info, query, isFirst);
624 isFirst = false;
625 }
626 }
627 }
628 catch (OrthancException&)
629 {
630 // This resource has probably been deleted during the find request
631 }
632 }
633
634 LOG(INFO) << "Number of candidate resources after filtering on all tags: " << answers.GetSize();
635
636 return true; // All the matching resources have been returned
637 } 329 }
638 } 330 }
639
640
641
642 /**
643 * TODO : Case-insensitive match for PN value representation (Patient
644 * Name). Case-senstive match for all the other value representations.
645 *
646 * Reference: DICOM PS 3.4
647 * - C.2.2.2.1 ("Single Value Matching")
648 * - C.2.2.2.4 ("Wild Card Matching")
649 * http://medical.nema.org/Dicom/2011/11_04pu.pdf
650 *
651 * "Except for Attributes with a PN Value Representation, only
652 * entities with values which match exactly the value specified in the
653 * request shall match. This matching is case-sensitive, i.e.,
654 * sensitive to the exact encoding of the key attribute value in
655 * character sets where a letter may have multiple encodings (e.g.,
656 * based on its case, its position in a word, or whether it is
657 * accented)
658 *
659 * For Attributes with a PN Value Representation (e.g., Patient Name
660 * (0010,0010)), an application may perform literal matching that is
661 * either case-sensitive, or that is insensitive to some or all
662 * aspects of case, position, accent, or other character encoding
663 * variants."
664 *
665 * (0008,0018) UI SOPInstanceUID => Case-sensitive
666 * (0008,0050) SH AccessionNumber => Case-sensitive
667 * (0010,0020) LO PatientID => Case-sensitive
668 * (0020,000D) UI StudyInstanceUID => Case-sensitive
669 * (0020,000E) UI SeriesInstanceUID => Case-sensitive
670 **/