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