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 "LookupResource.h"
36 
37 #include "../../Core/OrthancException.h"
38 #include "../../Core/FileStorage/StorageAccessor.h"
39 #include "../ServerToolbox.h"
40 #include "../../Core/DicomParsing/FromDcmtkBridge.h"
41 
42 
43 namespace Orthanc
44 {
DoesDicomMapMatch(const DicomMap & dicom,const DicomTag & tag,const IFindConstraint & constraint)45   static bool DoesDicomMapMatch(const DicomMap& dicom,
46                                 const DicomTag& tag,
47                                 const IFindConstraint& constraint)
48   {
49     const DicomValue* value = dicom.TestAndGetValue(tag);
50 
51     return (value != NULL &&
52             !value->IsNull() &&
53             !value->IsBinary() &&
54             constraint.Match(value->GetContent()));
55   }
56 
57 
Level(ResourceType level)58   LookupResource::Level::Level(ResourceType level) : level_(level)
59   {
60     const DicomTag* tags = NULL;
61     size_t size;
62 
63     ServerToolbox::LoadIdentifiers(tags, size, level);
64 
65     for (size_t i = 0; i < size; i++)
66     {
67       identifiers_.insert(tags[i]);
68     }
69 
70     DicomMap::LoadMainDicomTags(tags, size, level);
71 
72     for (size_t i = 0; i < size; i++)
73     {
74       if (identifiers_.find(tags[i]) == identifiers_.end())
75       {
76         mainTags_.insert(tags[i]);
77       }
78     }
79   }
80 
~Level()81   LookupResource::Level::~Level()
82   {
83     for (Constraints::iterator it = mainTagsConstraints_.begin();
84          it != mainTagsConstraints_.end(); ++it)
85     {
86       delete it->second;
87     }
88 
89     for (Constraints::iterator it = identifiersConstraints_.begin();
90          it != identifiersConstraints_.end(); ++it)
91     {
92       delete it->second;
93     }
94   }
95 
Add(const DicomTag & tag,std::auto_ptr<IFindConstraint> & constraint)96   bool LookupResource::Level::Add(const DicomTag& tag,
97                                   std::auto_ptr<IFindConstraint>& constraint)
98   {
99     if (identifiers_.find(tag) != identifiers_.end())
100     {
101       if (level_ == ResourceType_Patient)
102       {
103         // The filters on the patient level must be cloned to the study level
104         identifiersConstraints_[tag] = constraint->Clone();
105       }
106       else
107       {
108         identifiersConstraints_[tag] = constraint.release();
109       }
110 
111       return true;
112     }
113     else if (mainTags_.find(tag) != mainTags_.end())
114     {
115       if (level_ == ResourceType_Patient)
116       {
117         // The filters on the patient level must be cloned to the study level
118         mainTagsConstraints_[tag] = constraint->Clone();
119       }
120       else
121       {
122         mainTagsConstraints_[tag] = constraint.release();
123       }
124 
125       return true;
126     }
127     else
128     {
129       // This is not a main DICOM tag
130       return false;
131     }
132   }
133 
134 
IsMatch(const DicomMap & dicom) const135   bool LookupResource::Level::IsMatch(const DicomMap& dicom) const
136   {
137     for (Constraints::const_iterator it = identifiersConstraints_.begin();
138          it != identifiersConstraints_.end(); ++it)
139     {
140       assert(it->second != NULL);
141 
142       if (!DoesDicomMapMatch(dicom, it->first, *it->second))
143       {
144         return false;
145       }
146     }
147 
148     for (Constraints::const_iterator it = mainTagsConstraints_.begin();
149          it != mainTagsConstraints_.end(); ++it)
150     {
151       assert(it->second != NULL);
152 
153       if (!DoesDicomMapMatch(dicom, it->first, *it->second))
154       {
155         return false;
156       }
157     }
158 
159     return true;
160   }
161 
162 
LookupResource(ResourceType level)163   LookupResource::LookupResource(ResourceType level) : level_(level)
164   {
165     switch (level)
166     {
167       case ResourceType_Patient:
168         levels_[ResourceType_Patient] = new Level(ResourceType_Patient);
169         break;
170 
171       case ResourceType_Instance:
172         levels_[ResourceType_Instance] = new Level(ResourceType_Instance);
173         // Do not add "break" here
174 
175       case ResourceType_Series:
176         levels_[ResourceType_Series] = new Level(ResourceType_Series);
177         // Do not add "break" here
178 
179       case ResourceType_Study:
180         levels_[ResourceType_Study] = new Level(ResourceType_Study);
181         break;
182 
183       default:
184         throw OrthancException(ErrorCode_InternalError);
185     }
186   }
187 
188 
~LookupResource()189   LookupResource::~LookupResource()
190   {
191     for (Levels::iterator it = levels_.begin();
192          it != levels_.end(); ++it)
193     {
194       delete it->second;
195     }
196 
197     for (Constraints::iterator it = unoptimizedConstraints_.begin();
198          it != unoptimizedConstraints_.end(); ++it)
199     {
200       delete it->second;
201     }
202   }
203 
204 
205 
AddInternal(ResourceType level,const DicomTag & tag,std::auto_ptr<IFindConstraint> & constraint)206   bool LookupResource::AddInternal(ResourceType level,
207                                    const DicomTag& tag,
208                                    std::auto_ptr<IFindConstraint>& constraint)
209   {
210     Levels::iterator it = levels_.find(level);
211     if (it != levels_.end())
212     {
213       if (it->second->Add(tag, constraint))
214       {
215         return true;
216       }
217     }
218 
219     return false;
220   }
221 
222 
Add(const DicomTag & tag,IFindConstraint * constraint)223   void LookupResource::Add(const DicomTag& tag,
224                            IFindConstraint* constraint)
225   {
226     std::auto_ptr<IFindConstraint> c(constraint);
227 
228     if (!AddInternal(ResourceType_Patient, tag, c) &&
229         !AddInternal(ResourceType_Study, tag, c) &&
230         !AddInternal(ResourceType_Series, tag, c) &&
231         !AddInternal(ResourceType_Instance, tag, c))
232     {
233       unoptimizedConstraints_[tag] = c.release();
234     }
235   }
236 
237 
Match(const DicomMap & tags,const DicomTag & tag,const IFindConstraint & constraint)238   static bool Match(const DicomMap& tags,
239                     const DicomTag& tag,
240                     const IFindConstraint& constraint)
241   {
242     const DicomValue* value = tags.TestAndGetValue(tag);
243 
244     if (value == NULL ||
245         value->IsNull() ||
246         value->IsBinary())
247     {
248       return false;
249     }
250     else
251     {
252       return constraint.Match(value->GetContent());
253     }
254   }
255 
256 
Apply(SetOfResources & candidates,IDatabaseWrapper & database) const257   void LookupResource::Level::Apply(SetOfResources& candidates,
258                                     IDatabaseWrapper& database) const
259   {
260     // First, use the indexed identifiers
261     LookupIdentifierQuery query(level_);
262 
263     for (Constraints::const_iterator it = identifiersConstraints_.begin();
264          it != identifiersConstraints_.end(); ++it)
265     {
266       it->second->Setup(query, it->first);
267     }
268 
269     query.Apply(candidates, database);
270 
271     /*{
272       query.Print(std::cout);
273       std::list<int64_t>  source;
274       candidates.Flatten(source);
275       printf("=> %d\n", source.size());
276       }*/
277 
278     // Secondly, filter using the main DICOM tags
279     if (!identifiersConstraints_.empty() ||
280         !mainTagsConstraints_.empty())
281     {
282       std::list<int64_t>  source;
283       candidates.Flatten(source);
284       candidates.Clear();
285 
286       std::list<int64_t>  filtered;
287       for (std::list<int64_t>::const_iterator candidate = source.begin();
288            candidate != source.end(); ++candidate)
289       {
290         DicomMap tags;
291         database.GetMainDicomTags(tags, *candidate);
292 
293         bool match = true;
294 
295         // Re-apply the identifier constraints, as their "Setup"
296         // method is less restrictive than their "Match" method
297         for (Constraints::const_iterator it = identifiersConstraints_.begin();
298              match && it != identifiersConstraints_.end(); ++it)
299         {
300           if (!Match(tags, it->first, *it->second))
301           {
302             match = false;
303           }
304         }
305 
306         for (Constraints::const_iterator it = mainTagsConstraints_.begin();
307              match && it != mainTagsConstraints_.end(); ++it)
308         {
309           if (!Match(tags, it->first, *it->second))
310           {
311             match = false;
312           }
313         }
314 
315         if (match)
316         {
317           filtered.push_back(*candidate);
318         }
319       }
320 
321       candidates.Intersect(filtered);
322     }
323   }
324 
325 
326 
IsMatch(const DicomMap & dicom) const327   bool LookupResource::IsMatch(const DicomMap& dicom) const
328   {
329     for (Levels::const_iterator it = levels_.begin(); it != levels_.end(); ++it)
330     {
331       if (!it->second->IsMatch(dicom))
332       {
333         return false;
334       }
335     }
336 
337     for (Constraints::const_iterator it = unoptimizedConstraints_.begin();
338          it != unoptimizedConstraints_.end(); ++it)
339     {
340       assert(it->second != NULL);
341 
342       if (!DoesDicomMapMatch(dicom, it->first, *it->second))
343       {
344         return false;
345       }
346     }
347 
348     return true;
349   }
350 
351 
ApplyLevel(SetOfResources & candidates,ResourceType level,IDatabaseWrapper & database) const352   void LookupResource::ApplyLevel(SetOfResources& candidates,
353                                   ResourceType level,
354                                   IDatabaseWrapper& database) const
355   {
356     Levels::const_iterator it = levels_.find(level);
357     if (it != levels_.end())
358     {
359       it->second->Apply(candidates, database);
360     }
361 
362     if (level == ResourceType_Study &&
363         modalitiesInStudy_.get() != NULL)
364     {
365       // There is a constraint on the "ModalitiesInStudy" DICOM
366       // extension. Check out whether one child series has one of the
367       // allowed modalities
368       std::list<int64_t> allStudies, matchingStudies;
369       candidates.Flatten(allStudies);
370 
371       for (std::list<int64_t>::const_iterator
372              study = allStudies.begin(); study != allStudies.end(); ++study)
373       {
374         std::list<int64_t> childrenSeries;
375         database.GetChildrenInternalId(childrenSeries, *study);
376 
377         for (std::list<int64_t>::const_iterator
378                series = childrenSeries.begin(); series != childrenSeries.end(); ++series)
379         {
380           DicomMap tags;
381           database.GetMainDicomTags(tags, *series);
382 
383           const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY);
384           if (value != NULL &&
385               !value->IsNull() &&
386               !value->IsBinary())
387           {
388             if (modalitiesInStudy_->Match(value->GetContent()))
389             {
390               matchingStudies.push_back(*study);
391               break;
392             }
393           }
394         }
395       }
396 
397       candidates.Intersect(matchingStudies);
398     }
399   }
400 
401 
FindCandidates(std::list<int64_t> & result,IDatabaseWrapper & database) const402   void LookupResource::FindCandidates(std::list<int64_t>& result,
403                                       IDatabaseWrapper& database) const
404   {
405     ResourceType startingLevel;
406     if (level_ == ResourceType_Patient)
407     {
408       startingLevel = ResourceType_Patient;
409     }
410     else
411     {
412       startingLevel = ResourceType_Study;
413     }
414 
415     SetOfResources candidates(database, startingLevel);
416 
417     switch (level_)
418     {
419       case ResourceType_Patient:
420         ApplyLevel(candidates, ResourceType_Patient, database);
421         break;
422 
423       case ResourceType_Study:
424         ApplyLevel(candidates, ResourceType_Study, database);
425         break;
426 
427       case ResourceType_Series:
428         ApplyLevel(candidates, ResourceType_Study, database);
429         candidates.GoDown();
430         ApplyLevel(candidates, ResourceType_Series, database);
431         break;
432 
433       case ResourceType_Instance:
434         ApplyLevel(candidates, ResourceType_Study, database);
435         candidates.GoDown();
436         ApplyLevel(candidates, ResourceType_Series, database);
437         candidates.GoDown();
438         ApplyLevel(candidates, ResourceType_Instance, database);
439         break;
440 
441       default:
442         throw OrthancException(ErrorCode_InternalError);
443     }
444 
445     candidates.Flatten(result);
446   }
447 
448 
SetModalitiesInStudy(const std::string & modalities)449   void LookupResource::SetModalitiesInStudy(const std::string& modalities)
450   {
451     modalitiesInStudy_.reset(new ListConstraint(true /* case sensitive */));
452 
453     std::vector<std::string> items;
454     Toolbox::TokenizeString(items, modalities, '\\');
455 
456     for (size_t i = 0; i < items.size(); i++)
457     {
458       modalitiesInStudy_->AddAllowedValue(items[i]);
459     }
460   }
461 
462 
AddDicomConstraint(const DicomTag & tag,const std::string & dicomQuery,bool caseSensitive)463   void LookupResource::AddDicomConstraint(const DicomTag& tag,
464                                           const std::string& dicomQuery,
465                                           bool caseSensitive)
466   {
467     // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained
468     // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html
469     if (tag == DICOM_TAG_MODALITIES_IN_STUDY)
470     {
471       SetModalitiesInStudy(dicomQuery);
472     }
473     else
474     {
475       Add(tag, IFindConstraint::ParseDicomConstraint(tag, dicomQuery, caseSensitive));
476     }
477   }
478 
479 }
480