comparison OrthancServer/OrthancFindRequestHandler.cpp @ 758:67e6400fca03 query-retrieve

integration mainline -> query-retrieve
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 16 Apr 2014 16:34:09 +0200
parents 3bdb5db8e839 3596177682a9
children c2c28dd17e87
comparison
equal deleted inserted replaced
681:3bdb5db8e839 758:67e6400fca03
1 /** 1 /**
2 * Orthanc - A Lightweight, RESTful DICOM Store 2 * Orthanc - A Lightweight, RESTful DICOM Store
3 * Copyright (C) 2012-2013 Medical Physics Department, CHU of Liege, 3 * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
4 * Belgium 4 * 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
46 constraint.find('*') != std::string::npos || 46 constraint.find('*') != std::string::npos ||
47 constraint.find('\\') != std::string::npos || 47 constraint.find('\\') != std::string::npos ||
48 constraint.find('?') != std::string::npos); 48 constraint.find('?') != std::string::npos);
49 } 49 }
50 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, 51 static bool ApplyRangeConstraint(const std::string& value,
59 const std::string& constraint) 52 const std::string& constraint)
60 { 53 {
61 size_t separator = constraint.find('-'); 54 size_t separator = constraint.find('-');
62 std::string lower = ToLowerCase(constraint.substr(0, separator)); 55 std::string lower, upper, v;
63 std::string upper = ToLowerCase(constraint.substr(separator + 1)); 56 Toolbox::ToLowerCase(lower, constraint.substr(0, separator));
64 std::string v = ToLowerCase(value); 57 Toolbox::ToLowerCase(upper, constraint.substr(separator + 1));
58 Toolbox::ToLowerCase(v, value);
65 59
66 if (lower.size() == 0 && upper.size() == 0) 60 if (lower.size() == 0 && upper.size() == 0)
67 { 61 {
68 return false; 62 return false;
69 } 63 }
83 77
84 78
85 static bool ApplyListConstraint(const std::string& value, 79 static bool ApplyListConstraint(const std::string& value,
86 const std::string& constraint) 80 const std::string& constraint)
87 { 81 {
88 std::string v1 = ToLowerCase(value); 82 std::string v1;
83 Toolbox::ToLowerCase(v1, value);
89 84
90 std::vector<std::string> items; 85 std::vector<std::string> items;
91 Toolbox::TokenizeString(items, constraint, '\\'); 86 Toolbox::TokenizeString(items, constraint, '\\');
92 87
93 for (size_t i = 0; i < items.size(); i++) 88 for (size_t i = 0; i < items.size(); i++)
94 { 89 {
95 Toolbox::ToLowerCase(items[i]); 90 std::string lower;
96 if (items[i] == v1) 91 Toolbox::ToLowerCase(lower, items[i]);
92 if (lower == v1)
97 { 93 {
98 return true; 94 return true;
99 } 95 }
100 } 96 }
101 97
127 boost::regex::icase /* case insensitive search */); 123 boost::regex::icase /* case insensitive search */);
128 return boost::regex_match(value, pattern); 124 return boost::regex_match(value, pattern);
129 } 125 }
130 else 126 else
131 { 127 {
132 return ToLowerCase(value) == ToLowerCase(constraint); 128 std::string v, c;
129 Toolbox::ToLowerCase(v, value);
130 Toolbox::ToLowerCase(c, constraint);
131 return v == c;
133 } 132 }
134 } 133 }
135 134
136 135
137 static bool LookupOneInstance(std::string& result, 136 static bool LookupOneInstance(std::string& result,
208 std::string value; 207 std::string value;
209 if (resource.isMember(tag)) 208 if (resource.isMember(tag))
210 { 209 {
211 value = resource.get(tag, Json::arrayValue).get("Value", "").asString(); 210 value = resource.get(tag, Json::arrayValue).get("Value", "").asString();
212 result.SetValue(query.GetElement(i).GetTag(), value); 211 result.SetValue(query.GetElement(i).GetTag(), value);
212 }
213 else
214 {
215 result.SetValue(query.GetElement(i).GetTag(), "");
213 } 216 }
214 } 217 }
215 } 218 }
216 219
217 answers.Add(result); 220 answers.Add(result);
285 288
286 return true; 289 return true;
287 } 290 }
288 291
289 292
290 static bool LookupCandidateResourcesInternal(/* out */ std::list<std::string>& resources, 293 namespace
291 /* in */ ServerIndex& index, 294 {
292 /* in */ ResourceType level, 295 class CandidateResources
293 /* in */ const DicomMap& query, 296 {
294 /* in */ DicomTag tag) 297 private:
295 { 298 ServerIndex& index_;
296 if (query.HasTag(tag)) 299 ModalityManufacturer manufacturer_;
297 { 300 ResourceType level_;
298 const DicomValue& value = query.GetValue(tag); 301 bool isFilterApplied_;
299 if (!value.IsNull()) 302 std::set<std::string> filtered_;
300 { 303
301 std::string str = query.GetValue(tag).AsString(); 304 static void ListToSet(std::set<std::string>& target,
302 if (!IsWildcard(str)) 305 const std::list<std::string>& source)
303 { 306 {
304 index.LookupTagValue(resources, tag, str/*, level*/); 307 for (std::list<std::string>::const_iterator
305 return true; 308 it = source.begin(); it != source.end(); ++it)
306 } 309 {
307 } 310 target.insert(*it);
308 } 311 }
309 312 }
310 return false; 313
311 } 314 void ApplyExactFilter(const DicomTag& tag, const std::string& value)
312 315 {
313 316 LOG(INFO) << "Applying exact filter on tag "
314 static bool LookupCandidateResourcesInternal(/* inout */ std::set<std::string>& resources, 317 << FromDcmtkBridge::GetName(tag) << " (value: " << value << ")";
315 /* in */ bool alreadyFiltered, 318
316 /* in */ ServerIndex& index, 319 std::list<std::string> resources;
317 /* in */ ResourceType level, 320 index_.LookupTagValue(resources, tag, value, level_);
318 /* in */ const DicomMap& query, 321
319 /* in */ DicomTag tag) 322 if (isFilterApplied_)
320 { 323 {
321 assert(alreadyFiltered || resources.size() == 0); 324 std::set<std::string> s;
322 325 ListToSet(s, resources);
323 if (!query.HasTag(tag)) 326
324 { 327 std::set<std::string> tmp = filtered_;
325 return alreadyFiltered; 328 filtered_.clear();
326 } 329
327 330 for (std::set<std::string>::const_iterator
328 const DicomValue& value = query.GetValue(tag); 331 it = tmp.begin(); it != tmp.end(); ++it)
329 if (value.IsNull())
330 {
331 return alreadyFiltered;
332 }
333
334 std::string str = query.GetValue(tag).AsString();
335 if (IsWildcard(str))
336 {
337 return alreadyFiltered;
338 }
339
340 std::list<std::string> matches;
341 index.LookupTagValue(matches, tag, str/*, level*/);
342
343 if (alreadyFiltered)
344 {
345 std::set<std::string> previous = resources;
346
347 for (std::list<std::string>::const_iterator
348 it = matches.begin(); it != matches.end(); it++)
349 {
350 if (previous.find(*it) != previous.end())
351 {
352 resources.insert(*it);
353 }
354 }
355 }
356 else
357 {
358 for (std::list<std::string>::const_iterator
359 it = matches.begin(); it != matches.end(); it++)
360 {
361 resources.insert(*it);
362 }
363 }
364
365 return true;
366 }
367
368
369 static bool LookupCandidateResourcesAtOneLevel(/* out */ std::set<std::string>& resources,
370 /* in */ ServerIndex& index,
371 /* in */ ResourceType level,
372 /* in */ const DicomMap& fullQuery,
373 /* in */ ModalityManufacturer manufacturer)
374 {
375 DicomMap tmp;
376 fullQuery.ExtractMainDicomTagsForLevel(tmp, level);
377 DicomArray query(tmp);
378
379 if (query.GetSize() == 0)
380 {
381 return false;
382 }
383
384 for (size_t i = 0; i < query.GetSize(); i++)
385 {
386 const DicomTag tag = query.GetElement(i).GetTag();
387 const DicomValue& value = query.GetElement(i).GetValue();
388 if (!value.IsNull())
389 {
390 // TODO TODO TODO
391 }
392 }
393
394 printf(">>>>>>>>>>\n");
395 query.Print(stdout);
396 printf("<<<<<<<<<<\n\n");
397 return true;
398 }
399
400
401 static void LookupCandidateResources(/* out */ std::list<std::string>& resources,
402 /* in */ ServerIndex& index,
403 /* in */ ResourceType level,
404 /* in */ const DicomMap& query,
405 /* in */ ModalityManufacturer manufacturer)
406 {
407 #if 1
408 {
409 std::set<std::string> s;
410 LookupCandidateResourcesAtOneLevel(s, index, ResourceType_Patient, query, manufacturer);
411 LookupCandidateResourcesAtOneLevel(s, index, ResourceType_Study, query, manufacturer);
412 LookupCandidateResourcesAtOneLevel(s, index, ResourceType_Series, query, manufacturer);
413 LookupCandidateResourcesAtOneLevel(s, index, ResourceType_Instance, query, manufacturer);
414 }
415
416 std::set<std::string> filtered;
417 bool isFiltered = false;
418
419 // Filter by indexed tags, from most specific to least specific
420 //isFiltered = LookupCandidateResourcesInternal(filtered, isFiltered, index, level, query, DICOM_TAG_SOP_INSTANCE_UID);
421 isFiltered = LookupCandidateResourcesInternal(filtered, isFiltered, index, level, query, DICOM_TAG_SERIES_INSTANCE_UID);
422 //isFiltered = LookupCandidateResourcesInternal(filtered, isFiltered, index, level, query, DICOM_TAG_STUDY_INSTANCE_UID);
423 //isFiltered = LookupCandidateResourcesInternal(filtered, isFiltered, index, level, query, DICOM_TAG_PATIENT_ID);
424
425 resources.clear();
426
427 if (isFiltered)
428 {
429 for (std::set<std::string>::const_iterator
430 it = filtered.begin(); it != filtered.end(); it++)
431 {
432 resources.push_back(*it);
433 }
434 }
435 else
436 {
437 // No indexed tag matches the query. Return all the resources at this query level.
438 Json::Value allResources;
439 index.GetAllUuids(allResources, level);
440 assert(allResources.type() == Json::arrayValue);
441
442 for (Json::Value::ArrayIndex i = 0; i < allResources.size(); i++)
443 {
444 resources.push_back(allResources[i].asString());
445 }
446 }
447
448 #else
449
450 // TODO : Speed up using full querying against the MainDicomTags.
451
452 resources.clear();
453
454 bool done = false;
455
456 switch (level)
457 {
458 case ResourceType_Patient:
459 done = LookupCandidateResourcesInternal(resources, index, level, query, DICOM_TAG_PATIENT_ID);
460 break;
461
462 case ResourceType_Study:
463 done = LookupCandidateResourcesInternal(resources, index, level, query, DICOM_TAG_STUDY_INSTANCE_UID);
464 break;
465
466 case ResourceType_Series:
467 done = LookupCandidateResourcesInternal(resources, index, level, query, DICOM_TAG_SERIES_INSTANCE_UID);
468 break;
469
470 case ResourceType_Instance:
471 if (manufacturer == ModalityManufacturer_MedInria)
472 {
473 std::list<std::string> series;
474
475 if (LookupCandidateResourcesInternal(series, index, ResourceType_Series, query, DICOM_TAG_SERIES_INSTANCE_UID) &&
476 series.size() == 1)
477 { 332 {
478 index.GetChildInstances(resources, series.front()); 333 if (s.find(*it) != s.end())
479 done = true; 334 {
480 } 335 filtered_.insert(*it);
336 }
337 }
481 } 338 }
482 else 339 else
483 { 340 {
484 done = LookupCandidateResourcesInternal(resources, index, level, query, DICOM_TAG_SOP_INSTANCE_UID); 341 assert(filtered_.empty());
485 } 342 isFilterApplied_ = true;
486 343 ListToSet(filtered_, resources);
487 break; 344 }
488 345 }
489 default: 346
490 break; 347 public:
491 } 348 CandidateResources(ServerIndex& index,
492 349 ModalityManufacturer manufacturer) :
493 if (!done) 350 index_(index),
494 { 351 manufacturer_(manufacturer),
495 Json::Value allResources; 352 level_(ResourceType_Patient),
496 index.GetAllUuids(allResources, level); 353 isFilterApplied_(false)
497 assert(allResources.type() == Json::arrayValue); 354 {
498 355 }
499 for (Json::Value::ArrayIndex i = 0; i < allResources.size(); i++) 356
500 { 357 ResourceType GetLevel() const
501 resources.push_back(allResources[i].asString()); 358 {
502 } 359 return level_;
503 } 360 }
504 #endif 361
362 void GoDown()
363 {
364 assert(level_ != ResourceType_Instance);
365
366 if (isFilterApplied_)
367 {
368 std::set<std::string> tmp = filtered_;
369
370 filtered_.clear();
371
372 for (std::set<std::string>::const_iterator
373 it = tmp.begin(); it != tmp.end(); ++it)
374 {
375 std::list<std::string> children;
376 index_.GetChildren(children, *it);
377 ListToSet(filtered_, children);
378 }
379 }
380
381 switch (level_)
382 {
383 case ResourceType_Patient:
384 level_ = ResourceType_Study;
385 break;
386
387 case ResourceType_Study:
388 level_ = ResourceType_Series;
389 break;
390
391 case ResourceType_Series:
392 level_ = ResourceType_Instance;
393 break;
394
395 default:
396 throw OrthancException(ErrorCode_InternalError);
397 }
398 }
399
400 void Flatten(std::list<std::string>& resources) const
401 {
402 resources.clear();
403
404 if (isFilterApplied_)
405 {
406 for (std::set<std::string>::const_iterator
407 it = filtered_.begin(); it != filtered_.end(); ++it)
408 {
409 resources.push_back(*it);
410 }
411 }
412 else
413 {
414 Json::Value tmp;
415 index_.GetAllUuids(tmp, level_);
416 for (Json::Value::ArrayIndex i = 0; i < tmp.size(); i++)
417 {
418 resources.push_back(tmp[i].asString());
419 }
420 }
421 }
422
423 void ApplyFilter(const DicomTag& tag, const DicomMap& query)
424 {
425 if (query.HasTag(tag))
426 {
427 const DicomValue& value = query.GetValue(tag);
428 if (!value.IsNull())
429 {
430 std::string value = query.GetValue(tag).AsString();
431 if (!IsWildcard(value))
432 {
433 ApplyExactFilter(tag, value);
434 }
435 }
436 }
437 }
438 };
505 } 439 }
506 440
507 441
508 void OrthancFindRequestHandler::Handle(DicomFindAnswers& answers, 442 void OrthancFindRequestHandler::Handle(DicomFindAnswers& answers,
509 const DicomMap& input, 443 const DicomMap& input,
536 throw OrthancException(ErrorCode_BadRequest); 470 throw OrthancException(ErrorCode_BadRequest);
537 } 471 }
538 472
539 ResourceType level = StringToResourceType(levelTmp->AsString().c_str()); 473 ResourceType level = StringToResourceType(levelTmp->AsString().c_str());
540 474
541 switch (manufacturer) 475 if (level != ResourceType_Patient &&
542 { 476 level != ResourceType_Study &&
543 case ModalityManufacturer_MedInria: 477 level != ResourceType_Series &&
544 // MedInria makes FIND requests at the instance level before starting MOVE 478 level != ResourceType_Instance)
545 break; 479 {
546 480 throw OrthancException(ErrorCode_NotImplemented);
547 default: 481 }
548 if (level != ResourceType_Patient && 482
549 level != ResourceType_Study && 483
550 level != ResourceType_Series && 484 DicomArray query(input);
551 level != ResourceType_Instance) 485 LOG(INFO) << "DICOM C-Find request at level: " << EnumerationToString(level);
552 { 486
553 throw OrthancException(ErrorCode_NotImplemented); 487 for (size_t i = 0; i < query.GetSize(); i++)
554 } 488 {
489 if (!query.GetElement(i).GetValue().IsNull())
490 {
491 LOG(INFO) << " " << query.GetElement(i).GetTag()
492 << " " << FromDcmtkBridge::GetName(query.GetElement(i).GetTag())
493 << " = " << query.GetElement(i).GetValue().AsString();
494 }
555 } 495 }
556 496
557 497
558 /** 498 /**
559 * Retrieve the candidate resources for this query level. Whenever 499 * Retrieve the candidate resources for this query level. Whenever
560 * possible, we avoid returning ALL the resources for this query 500 * possible, we avoid returning ALL the resources for this query
561 * level, as it would imply reading the JSON file on the harddisk 501 * level, as it would imply reading the JSON file on the harddisk
562 * for each of them. 502 * for each of them.
563 **/ 503 **/
564 504
505 CandidateResources candidates(context_.GetIndex(), manufacturer);
506
507 for (;;)
508 {
509 switch (candidates.GetLevel())
510 {
511 case ResourceType_Patient:
512 candidates.ApplyFilter(DICOM_TAG_PATIENT_ID, input);
513 break;
514
515 case ResourceType_Study:
516 candidates.ApplyFilter(DICOM_TAG_STUDY_INSTANCE_UID, input);
517 candidates.ApplyFilter(DICOM_TAG_ACCESSION_NUMBER, input);
518 break;
519
520 case ResourceType_Series:
521 candidates.ApplyFilter(DICOM_TAG_SERIES_INSTANCE_UID, input);
522 break;
523
524 case ResourceType_Instance:
525 candidates.ApplyFilter(DICOM_TAG_SOP_INSTANCE_UID, input);
526 break;
527
528 default:
529 throw OrthancException(ErrorCode_InternalError);
530 }
531
532 if (candidates.GetLevel() == level)
533 {
534 break;
535 }
536
537 candidates.GoDown();
538 }
539
565 std::list<std::string> resources; 540 std::list<std::string> resources;
566 LookupCandidateResources(resources, context_.GetIndex(), level, input, manufacturer); 541 candidates.Flatten(resources);
567 542
543 LOG(INFO) << "Number of candidate resources after exact filtering: " << resources.size();
568 544
569 /** 545 /**
570 * Apply filtering on modalities for studies, if asked (this is an 546 * Apply filtering on modalities for studies, if asked (this is an
571 * extension to standard DICOM) 547 * extension to standard DICOM)
572 * http://www.medicalconnections.co.uk/kb/Filtering_on_and_Retrieving_the_Modality_in_a_C_FIND 548 * http://www.medicalconnections.co.uk/kb/Filtering_on_and_Retrieving_the_Modality_in_a_C_FIND
584 560
585 561
586 /** 562 /**
587 * Loop over all the resources for this query level. 563 * Loop over all the resources for this query level.
588 **/ 564 **/
589
590 DicomArray query(input);
591 query.Print(stdout);
592 565
593 for (std::list<std::string>::const_iterator 566 for (std::list<std::string>::const_iterator
594 resource = resources.begin(); resource != resources.end(); ++resource) 567 resource = resources.begin(); resource != resources.end(); ++resource)
595 { 568 {
596 try 569 try
612 // This resource has probably been deleted during the find request 585 // This resource has probably been deleted during the find request
613 } 586 }
614 } 587 }
615 } 588 }
616 } 589 }
590
591
592
593 /**
594 * TODO : Case-insensitive match for PN value representation (Patient
595 * Name). Case-senstive match for all the other value representations.
596 *
597 * Reference: DICOM PS 3.4
598 * - C.2.2.2.1 ("Single Value Matching")
599 * - C.2.2.2.4 ("Wild Card Matching")
600 * http://medical.nema.org/Dicom/2011/11_04pu.pdf (
601 **/