1 /** 2 * Orthanc - A Lightweight, RESTful DICOM Store 3 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics 4 * Department, University Hospital of Liege, Belgium 5 * Copyright (C) 2017-2021 Osimis S.A., Belgium 6 * 7 * This program is free software: you can redistribute it and/or 8 * modify it under the terms of the GNU General Public License as 9 * published by the Free Software Foundation, either version 3 of the 10 * License, or (at your option) any later version. 11 * 12 * In addition, as a special exception, the copyright holders of this 13 * program give permission to link the code of its release with the 14 * OpenSSL project's "OpenSSL" library (or with modified versions of it 15 * that use the same license as the "OpenSSL" library), and distribute 16 * the linked executables. You must obey the GNU General Public License 17 * in all respects for all of the code used other than "OpenSSL". If you 18 * modify file(s) with this exception, you may extend this exception to 19 * your version of the file(s), but you are not obligated to do so. If 20 * you do not wish to do so, delete this exception statement from your 21 * version. If you delete this exception statement from all source files 22 * in the program, then also delete it here. 23 * 24 * This program is distributed in the hope that it will be useful, but 25 * WITHOUT ANY WARRANTY; without even the implied warranty of 26 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 27 * General Public License for more details. 28 * 29 * You should have received a copy of the GNU General Public License 30 * along with this program. If not, see <http://www.gnu.org/licenses/>. 31 **/ 32 33 34 #include "PrecompiledHeadersServer.h" 35 #include "OrthancFindRequestHandler.h" 36 37 #include "../../OrthancFramework/Sources/DicomFormat/DicomArray.h" 38 #include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" 39 #include "../../OrthancFramework/Sources/Logging.h" 40 #include "../../OrthancFramework/Sources/Lua/LuaFunctionCall.h" 41 #include "../../OrthancFramework/Sources/MetricsRegistry.h" 42 #include "OrthancConfiguration.h" 43 #include "Search/DatabaseLookup.h" 44 #include "ServerContext.h" 45 #include "ServerToolbox.h" 46 47 #include <boost/regex.hpp> 48 49 50 namespace Orthanc 51 { GetChildren(std::list<std::string> & target,ServerIndex & index,const std::list<std::string> & source)52 static void GetChildren(std::list<std::string>& target, 53 ServerIndex& index, 54 const std::list<std::string>& source) 55 { 56 target.clear(); 57 58 for (std::list<std::string>::const_iterator 59 it = source.begin(); it != source.end(); ++it) 60 { 61 std::list<std::string> tmp; 62 index.GetChildren(tmp, *it); 63 target.splice(target.end(), tmp); 64 } 65 } 66 67 StoreSetOfStrings(DicomMap & result,const DicomTag & tag,const std::set<std::string> & values)68 static void StoreSetOfStrings(DicomMap& result, 69 const DicomTag& tag, 70 const std::set<std::string>& values) 71 { 72 bool isFirst = true; 73 74 std::string s; 75 for (std::set<std::string>::const_iterator 76 it = values.begin(); it != values.end(); ++it) 77 { 78 if (isFirst) 79 { 80 isFirst = false; 81 } 82 else 83 { 84 s += "\\"; 85 } 86 87 s += *it; 88 } 89 90 result.SetValue(tag, s, false); 91 } 92 93 ComputePatientCounters(DicomMap & result,ServerIndex & index,const std::string & patient,const DicomMap & query)94 static void ComputePatientCounters(DicomMap& result, 95 ServerIndex& index, 96 const std::string& patient, 97 const DicomMap& query) 98 { 99 std::list<std::string> studies; 100 index.GetChildren(studies, patient); 101 102 if (query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES)) 103 { 104 result.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES, 105 boost::lexical_cast<std::string>(studies.size()), false); 106 } 107 108 if (!query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES) && 109 !query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES)) 110 { 111 return; 112 } 113 114 std::list<std::string> series; 115 GetChildren(series, index, studies); 116 studies.clear(); // This information is useless below 117 118 if (query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES)) 119 { 120 result.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES, 121 boost::lexical_cast<std::string>(series.size()), false); 122 } 123 124 if (!query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES)) 125 { 126 return; 127 } 128 129 std::list<std::string> instances; 130 GetChildren(instances, index, series); 131 132 if (query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES)) 133 { 134 result.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES, 135 boost::lexical_cast<std::string>(instances.size()), false); 136 } 137 } 138 139 ComputeStudyCounters(DicomMap & result,ServerContext & context,const std::string & study,const DicomMap & query)140 static void ComputeStudyCounters(DicomMap& result, 141 ServerContext& context, 142 const std::string& study, 143 const DicomMap& query) 144 { 145 ServerIndex& index = context.GetIndex(); 146 147 std::list<std::string> series; 148 index.GetChildren(series, study); 149 150 if (query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES)) 151 { 152 result.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES, 153 boost::lexical_cast<std::string>(series.size()), false); 154 } 155 156 if (query.HasTag(DICOM_TAG_MODALITIES_IN_STUDY)) 157 { 158 std::set<std::string> values; 159 160 for (std::list<std::string>::const_iterator 161 it = series.begin(); it != series.end(); ++it) 162 { 163 DicomMap tags; 164 if (index.GetMainDicomTags(tags, *it, ResourceType_Series, ResourceType_Series)) 165 { 166 const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY); 167 168 if (value != NULL && 169 !value->IsNull() && 170 !value->IsBinary()) 171 { 172 values.insert(value->GetContent()); 173 } 174 } 175 } 176 177 StoreSetOfStrings(result, DICOM_TAG_MODALITIES_IN_STUDY, values); 178 } 179 180 if (!query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) && 181 !query.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY)) 182 { 183 return; 184 } 185 186 std::list<std::string> instances; 187 GetChildren(instances, index, series); 188 189 if (query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES)) 190 { 191 result.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES, 192 boost::lexical_cast<std::string>(instances.size()), false); 193 } 194 195 if (query.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY)) 196 { 197 std::set<std::string> values; 198 199 for (std::list<std::string>::const_iterator 200 it = instances.begin(); it != instances.end(); ++it) 201 { 202 std::string value; 203 if (context.LookupOrReconstructMetadata(value, *it, ResourceType_Instance, MetadataType_Instance_SopClassUid)) 204 { 205 values.insert(value); 206 } 207 } 208 209 StoreSetOfStrings(result, DICOM_TAG_SOP_CLASSES_IN_STUDY, values); 210 } 211 } 212 213 ComputeSeriesCounters(DicomMap & result,ServerIndex & index,const std::string & series,const DicomMap & query)214 static void ComputeSeriesCounters(DicomMap& result, 215 ServerIndex& index, 216 const std::string& series, 217 const DicomMap& query) 218 { 219 std::list<std::string> instances; 220 index.GetChildren(instances, series); 221 222 if (query.HasTag(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES)) 223 { 224 result.SetValue(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES, 225 boost::lexical_cast<std::string>(instances.size()), false); 226 } 227 } 228 229 ComputeCounters(ServerContext & context,const std::string & instanceId,ResourceType level,const DicomMap & query)230 static DicomMap* ComputeCounters(ServerContext& context, 231 const std::string& instanceId, 232 ResourceType level, 233 const DicomMap& query) 234 { 235 switch (level) 236 { 237 case ResourceType_Patient: 238 if (!query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES) && 239 !query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES) && 240 !query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES)) 241 { 242 return NULL; 243 } 244 245 break; 246 247 case ResourceType_Study: 248 if (!query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES) && 249 !query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) && 250 !query.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY) && 251 !query.HasTag(DICOM_TAG_MODALITIES_IN_STUDY)) 252 { 253 return NULL; 254 } 255 256 break; 257 258 case ResourceType_Series: 259 if (!query.HasTag(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES)) 260 { 261 return NULL; 262 } 263 264 break; 265 266 default: 267 return NULL; 268 } 269 270 std::string parent; 271 if (!context.GetIndex().LookupParent(parent, instanceId, level)) 272 { 273 throw OrthancException(ErrorCode_UnknownResource); // The resource was deleted in between 274 } 275 276 std::unique_ptr<DicomMap> result(new DicomMap); 277 278 switch (level) 279 { 280 case ResourceType_Patient: 281 ComputePatientCounters(*result, context.GetIndex(), parent, query); 282 break; 283 284 case ResourceType_Study: 285 ComputeStudyCounters(*result, context, parent, query); 286 break; 287 288 case ResourceType_Series: 289 ComputeSeriesCounters(*result, context.GetIndex(), parent, query); 290 break; 291 292 default: 293 throw OrthancException(ErrorCode_InternalError); 294 } 295 296 return result.release(); 297 } 298 299 AddAnswer(DicomFindAnswers & answers,const DicomMap & mainDicomTags,const Json::Value * dicomAsJson,const DicomArray & query,const std::list<DicomTag> & sequencesToReturn,const DicomMap * counters,const std::string & defaultPrivateCreator,const std::map<uint16_t,std::string> & privateCreators,const std::string & retrieveAet)300 static void AddAnswer(DicomFindAnswers& answers, 301 const DicomMap& mainDicomTags, 302 const Json::Value* dicomAsJson, 303 const DicomArray& query, 304 const std::list<DicomTag>& sequencesToReturn, 305 const DicomMap* counters, 306 const std::string& defaultPrivateCreator, 307 const std::map<uint16_t, std::string>& privateCreators, 308 const std::string& retrieveAet) 309 { 310 DicomMap match; 311 312 if (dicomAsJson != NULL) 313 { 314 match.FromDicomAsJson(*dicomAsJson); 315 } 316 else 317 { 318 match.Assign(mainDicomTags); 319 } 320 321 DicomMap result; 322 323 /** 324 * Add the mandatory "Retrieve AE Title (0008,0054)" tag, which was missing in Orthanc <= 1.7.2. 325 * http://dicom.nema.org/medical/dicom/current/output/html/part04.html#sect_C.4.1.1.3.2 326 * https://groups.google.com/g/orthanc-users/c/-7zNTKR_PMU/m/kfjwzEVNAgAJ 327 **/ 328 result.SetValue(DICOM_TAG_RETRIEVE_AE_TITLE, retrieveAet, false /* not binary */); 329 330 for (size_t i = 0; i < query.GetSize(); i++) 331 { 332 if (query.GetElement(i).GetTag() == DICOM_TAG_QUERY_RETRIEVE_LEVEL) 333 { 334 // Fix issue 30 on Google Code (QR response missing "Query/Retrieve Level" (008,0052)) 335 result.SetValue(query.GetElement(i).GetTag(), query.GetElement(i).GetValue()); 336 } 337 else if (query.GetElement(i).GetTag() == DICOM_TAG_SPECIFIC_CHARACTER_SET) 338 { 339 // Do not include the encoding, this is handled by class ParsedDicomFile 340 } 341 else 342 { 343 const DicomTag& tag = query.GetElement(i).GetTag(); 344 const DicomValue* value = match.TestAndGetValue(tag); 345 346 if (value != NULL && 347 !value->IsNull() && 348 !value->IsBinary()) 349 { 350 result.SetValue(tag, value->GetContent(), false); 351 } 352 else 353 { 354 result.SetValue(tag, "", false); 355 } 356 } 357 } 358 359 if (counters != NULL) 360 { 361 DicomArray tmp(*counters); 362 for (size_t i = 0; i < tmp.GetSize(); i++) 363 { 364 result.SetValue(tmp.GetElement(i).GetTag(), tmp.GetElement(i).GetValue().GetContent(), false); 365 } 366 } 367 368 if (result.GetSize() == 0 && 369 sequencesToReturn.empty()) 370 { 371 CLOG(WARNING, DICOM) << "The C-FIND request does not return any DICOM tag"; 372 } 373 else if (sequencesToReturn.empty()) 374 { 375 answers.Add(result); 376 } 377 else if (dicomAsJson == NULL) 378 { 379 CLOG(WARNING, DICOM) << "C-FIND query requesting a sequence, but reading JSON from disk is disabled"; 380 answers.Add(result); 381 } 382 else 383 { 384 ParsedDicomFile dicom(result, GetDefaultDicomEncoding(), 385 true /* be permissive, cf. issue #136 */, defaultPrivateCreator, privateCreators); 386 387 for (std::list<DicomTag>::const_iterator tag = sequencesToReturn.begin(); 388 tag != sequencesToReturn.end(); ++tag) 389 { 390 assert(dicomAsJson != NULL); 391 const Json::Value& source = (*dicomAsJson) [tag->Format()]; 392 393 if (source.type() == Json::objectValue && 394 source.isMember("Type") && 395 source.isMember("Value") && 396 source["Type"].asString() == "Sequence" && 397 source["Value"].type() == Json::arrayValue) 398 { 399 Json::Value content = Json::arrayValue; 400 401 for (Json::Value::ArrayIndex i = 0; i < source["Value"].size(); i++) 402 { 403 Json::Value item; 404 Toolbox::SimplifyDicomAsJson(item, source["Value"][i], DicomToJsonFormat_Short); 405 content.append(item); 406 } 407 408 if (tag->IsPrivate()) 409 { 410 std::map<uint16_t, std::string>::const_iterator found = privateCreators.find(tag->GetGroup()); 411 412 if (found != privateCreators.end()) 413 { 414 dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, found->second.c_str()); 415 } 416 else 417 { 418 dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, defaultPrivateCreator); 419 } 420 } 421 else 422 { 423 dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, "" /* no private creator */); 424 } 425 } 426 } 427 428 answers.Add(dicom); 429 } 430 } 431 432 433 FilterQueryTag(std::string & value,ResourceType level,const DicomTag & tag,ModalityManufacturer manufacturer)434 bool OrthancFindRequestHandler::FilterQueryTag(std::string& value /* can be modified */, 435 ResourceType level, 436 const DicomTag& tag, 437 ModalityManufacturer manufacturer) 438 { 439 // Whatever the manufacturer, remove the GenericGroupLength tags 440 // http://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_7.2.html 441 // https://bugs.orthanc-server.com/show_bug.cgi?id=31 442 if (tag.GetElement() == 0x0000) 443 { 444 return false; 445 } 446 447 switch (manufacturer) 448 { 449 case ModalityManufacturer_Vitrea: 450 // Following Denis Nesterov's mail on 2015-11-30 451 if (tag == DicomTag(0x5653, 0x0010)) // "PrivateCreator = Vital Images SW 3.4" 452 { 453 return false; 454 } 455 456 break; 457 458 default: 459 break; 460 } 461 462 return true; 463 } 464 465 ApplyLuaFilter(DicomMap & target,const DicomMap & source,const std::string & remoteIp,const std::string & remoteAet,const std::string & calledAet,ModalityManufacturer manufacturer)466 bool OrthancFindRequestHandler::ApplyLuaFilter(DicomMap& target, 467 const DicomMap& source, 468 const std::string& remoteIp, 469 const std::string& remoteAet, 470 const std::string& calledAet, 471 ModalityManufacturer manufacturer) 472 { 473 static const char* LUA_CALLBACK = "IncomingFindRequestFilter"; 474 475 LuaScripting::Lock lock(context_.GetLuaScripting()); 476 477 if (!lock.GetLua().IsExistingFunction(LUA_CALLBACK)) 478 { 479 return false; 480 } 481 else 482 { 483 Json::Value origin; 484 FormatOrigin(origin, remoteIp, remoteAet, calledAet, manufacturer); 485 486 LuaFunctionCall call(lock.GetLua(), LUA_CALLBACK); 487 call.PushDicom(source); 488 call.PushJson(origin); 489 call.ExecuteToDicom(target); 490 491 return true; 492 } 493 } 494 495 OrthancFindRequestHandler(ServerContext & context)496 OrthancFindRequestHandler::OrthancFindRequestHandler(ServerContext& context) : 497 context_(context), 498 maxResults_(0), 499 maxInstances_(0) 500 { 501 } 502 503 504 class OrthancFindRequestHandler::LookupVisitor : public ServerContext::ILookupVisitor 505 { 506 private: 507 DicomFindAnswers& answers_; 508 ServerContext& context_; 509 ResourceType level_; 510 const DicomMap& query_; 511 DicomArray queryAsArray_; 512 const std::list<DicomTag>& sequencesToReturn_; 513 std::string defaultPrivateCreator_; // the private creator to use if the group is not defined in the query itself 514 const std::map<uint16_t, std::string>& privateCreators_; // the private creators defined in the query itself 515 std::string retrieveAet_; 516 517 public: LookupVisitor(DicomFindAnswers & answers,ServerContext & context,ResourceType level,const DicomMap & query,const std::list<DicomTag> & sequencesToReturn,const std::map<uint16_t,std::string> & privateCreators)518 LookupVisitor(DicomFindAnswers& answers, 519 ServerContext& context, 520 ResourceType level, 521 const DicomMap& query, 522 const std::list<DicomTag>& sequencesToReturn, 523 const std::map<uint16_t, std::string>& privateCreators) : 524 answers_(answers), 525 context_(context), 526 level_(level), 527 query_(query), 528 queryAsArray_(query), 529 sequencesToReturn_(sequencesToReturn), 530 privateCreators_(privateCreators) 531 { 532 answers_.SetComplete(false); 533 534 { 535 OrthancConfiguration::ReaderLock lock; 536 defaultPrivateCreator_ = lock.GetConfiguration().GetDefaultPrivateCreator(); 537 retrieveAet_ = lock.GetConfiguration().GetOrthancAET(); 538 } 539 } 540 IsDicomAsJsonNeeded() const541 virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE 542 { 543 // Ask the "DICOM-as-JSON" attachment only if sequences are to 544 // be returned OR if "query_" contains non-main DICOM tags! 545 546 DicomMap withoutSpecialTags; 547 withoutSpecialTags.Assign(query_); 548 549 // Check out "ComputeCounters()" 550 withoutSpecialTags.Remove(DICOM_TAG_MODALITIES_IN_STUDY); 551 withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES); 552 withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES); 553 withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES); 554 withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES); 555 withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES); 556 withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES); 557 withoutSpecialTags.Remove(DICOM_TAG_SOP_CLASSES_IN_STUDY); 558 559 // Check out "AddAnswer()" 560 withoutSpecialTags.Remove(DICOM_TAG_SPECIFIC_CHARACTER_SET); 561 withoutSpecialTags.Remove(DICOM_TAG_QUERY_RETRIEVE_LEVEL); 562 563 return (!sequencesToReturn_.empty() || 564 !withoutSpecialTags.HasOnlyMainDicomTags()); 565 } 566 MarkAsComplete()567 virtual void MarkAsComplete() ORTHANC_OVERRIDE 568 { 569 answers_.SetComplete(true); 570 } 571 Visit(const std::string & publicId,const std::string & instanceId,const DicomMap & mainDicomTags,const Json::Value * dicomAsJson)572 virtual void Visit(const std::string& publicId, 573 const std::string& instanceId, 574 const DicomMap& mainDicomTags, 575 const Json::Value* dicomAsJson) ORTHANC_OVERRIDE 576 { 577 std::unique_ptr<DicomMap> counters(ComputeCounters(context_, instanceId, level_, query_)); 578 579 AddAnswer(answers_, mainDicomTags, dicomAsJson, queryAsArray_, sequencesToReturn_, 580 counters.get(), defaultPrivateCreator_, privateCreators_, retrieveAet_); 581 } 582 }; 583 584 Handle(DicomFindAnswers & answers,const DicomMap & input,const std::list<DicomTag> & sequencesToReturn,const std::string & remoteIp,const std::string & remoteAet,const std::string & calledAet,ModalityManufacturer manufacturer)585 void OrthancFindRequestHandler::Handle(DicomFindAnswers& answers, 586 const DicomMap& input, 587 const std::list<DicomTag>& sequencesToReturn, 588 const std::string& remoteIp, 589 const std::string& remoteAet, 590 const std::string& calledAet, 591 ModalityManufacturer manufacturer) 592 { 593 MetricsRegistry::Timer timer(context_.GetMetricsRegistry(), "orthanc_find_scp_duration_ms"); 594 595 596 /** 597 * Deal with global configuration 598 **/ 599 600 bool caseSensitivePN; 601 602 { 603 OrthancConfiguration::ReaderLock lock; 604 caseSensitivePN = lock.GetConfiguration().GetBooleanParameter("CaseSensitivePN", false); 605 606 RemoteModalityParameters remote; 607 if (!lock.GetConfiguration().LookupDicomModalityUsingAETitle(remote, remoteAet)) 608 { 609 if (lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowFind", false)) 610 { 611 CLOG(INFO, DICOM) << "C-FIND: Allowing SCU request from unknown modality with AET: " << remoteAet; 612 } 613 else 614 { 615 // This should never happen, given the test at bottom of 616 // "OrthancApplicationEntityFilter::IsAllowedRequest()" 617 throw OrthancException(ErrorCode_InexistentItem, 618 "C-FIND: Rejecting SCU request from unknown modality with AET: " + remoteAet); 619 } 620 } 621 } 622 623 624 625 /** 626 * Possibly apply the user-supplied Lua filter. 627 **/ 628 629 DicomMap lua; 630 const DicomMap* filteredInput = &input; 631 632 if (ApplyLuaFilter(lua, input, remoteIp, remoteAet, calledAet, manufacturer)) 633 { 634 filteredInput = &lua; 635 } 636 637 638 /** 639 * Retrieve the query level. 640 **/ 641 642 assert(filteredInput != NULL); 643 const DicomValue* levelTmp = filteredInput->TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL); 644 if (levelTmp == NULL || 645 levelTmp->IsNull() || 646 levelTmp->IsBinary()) 647 { 648 throw OrthancException(ErrorCode_BadRequest, 649 "C-FIND request without the tag 0008,0052 (QueryRetrieveLevel)"); 650 } 651 652 ResourceType level = StringToResourceType(levelTmp->GetContent().c_str()); 653 654 if (level != ResourceType_Patient && 655 level != ResourceType_Study && 656 level != ResourceType_Series && 657 level != ResourceType_Instance) 658 { 659 throw OrthancException(ErrorCode_NotImplemented); 660 } 661 662 663 DicomArray query(*filteredInput); 664 CLOG(INFO, DICOM) << "DICOM C-Find request at level: " << EnumerationToString(level); 665 666 for (size_t i = 0; i < query.GetSize(); i++) 667 { 668 if (!query.GetElement(i).GetValue().IsNull()) 669 { 670 CLOG(INFO, DICOM) << " (" << query.GetElement(i).GetTag().Format() 671 << ") " << FromDcmtkBridge::GetTagName(query.GetElement(i)) 672 << " = " << context_.GetDeidentifiedContent(query.GetElement(i)); 673 } 674 } 675 676 for (std::list<DicomTag>::const_iterator it = sequencesToReturn.begin(); 677 it != sequencesToReturn.end(); ++it) 678 { 679 CLOG(INFO, DICOM) << " (" << it->Format() 680 << ") " << FromDcmtkBridge::GetTagName(*it, "") 681 << " : sequence tag whose content will be copied"; 682 } 683 684 // collect the private creators from the query itself 685 std::map<uint16_t, std::string> privateCreators; 686 for (size_t i = 0; i < query.GetSize(); i++) 687 { 688 const DicomElement& element = query.GetElement(i); 689 if (element.GetTag().IsPrivate() && element.GetTag().GetElement() == 0x10) 690 { 691 privateCreators[element.GetTag().GetGroup()] = element.GetValue().GetContent(); 692 } 693 } 694 695 /** 696 * Build up the query object. 697 **/ 698 699 DatabaseLookup lookup; 700 701 for (size_t i = 0; i < query.GetSize(); i++) 702 { 703 const DicomElement& element = query.GetElement(i); 704 const DicomTag tag = element.GetTag(); 705 706 if (element.GetValue().IsNull() || 707 tag == DICOM_TAG_QUERY_RETRIEVE_LEVEL || 708 tag == DICOM_TAG_SPECIFIC_CHARACTER_SET) 709 { 710 continue; 711 } 712 713 std::string value = element.GetValue().GetContent(); 714 if (value.size() == 0) 715 { 716 // An empty string corresponds to an universal constraint, so we ignore it 717 continue; 718 } 719 720 if (FilterQueryTag(value, level, tag, manufacturer)) 721 { 722 ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag); 723 724 // DICOM specifies that searches must be case sensitive, except 725 // for tags with a PN value representation 726 bool sensitive = true; 727 if (vr == ValueRepresentation_PersonName) 728 { 729 sensitive = caseSensitivePN; 730 } 731 732 lookup.AddDicomConstraint(tag, value, sensitive, true /* mandatory */); 733 } 734 else 735 { 736 CLOG(INFO, DICOM) << "Because of a patch for the manufacturer of the remote modality, " 737 << "ignoring constraint on tag (" << tag.Format() << ") " 738 << FromDcmtkBridge::GetTagName(element); 739 } 740 } 741 742 743 /** 744 * Run the query. 745 **/ 746 747 size_t limit = (level == ResourceType_Instance) ? maxInstances_ : maxResults_; 748 749 750 LookupVisitor visitor(answers, context_, level, *filteredInput, sequencesToReturn, privateCreators); 751 context_.Apply(visitor, lookup, level, 0 /* "since" is not relevant to C-FIND */, limit); 752 } 753 754 FormatOrigin(Json::Value & origin,const std::string & remoteIp,const std::string & remoteAet,const std::string & calledAet,ModalityManufacturer manufacturer)755 void OrthancFindRequestHandler::FormatOrigin(Json::Value& origin, 756 const std::string& remoteIp, 757 const std::string& remoteAet, 758 const std::string& calledAet, 759 ModalityManufacturer manufacturer) 760 { 761 origin = Json::objectValue; 762 origin["RemoteIp"] = remoteIp; 763 origin["RemoteAet"] = remoteAet; 764 origin["CalledAet"] = calledAet; 765 origin["Manufacturer"] = EnumerationToString(manufacturer); 766 } 767 } 768