Mercurial > hg > orthanc
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 **/ |