1360
|
1 /**
|
|
2 * Orthanc - A Lightweight, RESTful DICOM Store
|
|
3 * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
|
|
4 * Department, University Hospital of Liege, Belgium
|
|
5 *
|
|
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
|
|
8 * published by the Free Software Foundation, either version 3 of the
|
|
9 * License, or (at your option) any later version.
|
|
10 *
|
|
11 * In addition, as a special exception, the copyright holders of this
|
|
12 * program give permission to link the code of its release with the
|
|
13 * OpenSSL project's "OpenSSL" library (or with modified versions of it
|
|
14 * that use the same license as the "OpenSSL" library), and distribute
|
|
15 * the linked executables. You must obey the GNU General Public License
|
|
16 * in all respects for all of the code used other than "OpenSSL". If you
|
|
17 * modify file(s) with this exception, you may extend this exception to
|
|
18 * your version of the file(s), but you are not obligated to do so. If
|
|
19 * you do not wish to do so, delete this exception statement from your
|
|
20 * version. If you delete this exception statement from all source files
|
|
21 * in the program, then also delete it here.
|
|
22 *
|
|
23 * This program is distributed in the hope that it will be useful, but
|
|
24 * WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
25 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
26 * General Public License for more details.
|
|
27 *
|
|
28 * You should have received a copy of the GNU General Public License
|
|
29 * along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
30 **/
|
|
31
|
|
32
|
|
33
|
|
34 #include "PrecompiledHeadersServer.h"
|
|
35 #include "DicomFindQuery.h"
|
|
36
|
|
37 #include "FromDcmtkBridge.h"
|
|
38
|
|
39 #include <boost/regex.hpp>
|
|
40
|
|
41
|
|
42 namespace Orthanc
|
|
43 {
|
|
44 class DicomFindQuery::ValueConstraint : public DicomFindQuery::IConstraint
|
|
45 {
|
|
46 private:
|
|
47 bool isCaseSensitive_;
|
|
48 std::string expected_;
|
|
49
|
|
50 public:
|
|
51 ValueConstraint(const std::string& value,
|
|
52 bool caseSensitive) :
|
|
53 isCaseSensitive_(caseSensitive),
|
|
54 expected_(value)
|
|
55 {
|
|
56 }
|
|
57
|
|
58 const std::string& GetValue() const
|
|
59 {
|
|
60 return expected_;
|
|
61 }
|
|
62
|
|
63 virtual bool IsExactConstraint() const
|
|
64 {
|
|
65 return isCaseSensitive_;
|
|
66 }
|
|
67
|
|
68 virtual bool Apply(const std::string& value) const
|
|
69 {
|
|
70 if (isCaseSensitive_)
|
|
71 {
|
|
72 return expected_ == value;
|
|
73 }
|
|
74 else
|
|
75 {
|
|
76 std::string v, c;
|
|
77 Toolbox::ToLowerCase(v, value);
|
|
78 Toolbox::ToLowerCase(c, expected_);
|
|
79 return v == c;
|
|
80 }
|
|
81 }
|
|
82 };
|
|
83
|
|
84
|
|
85 class DicomFindQuery::ListConstraint : public DicomFindQuery::IConstraint
|
|
86 {
|
|
87 private:
|
|
88 std::set<std::string> values_;
|
|
89
|
|
90 public:
|
|
91 ListConstraint(const std::string& values)
|
|
92 {
|
|
93 std::vector<std::string> items;
|
|
94 Toolbox::TokenizeString(items, values, '\\');
|
|
95
|
|
96 for (size_t i = 0; i < items.size(); i++)
|
|
97 {
|
|
98 std::string lower;
|
|
99 Toolbox::ToLowerCase(lower, items[i]);
|
|
100 values_.insert(lower);
|
|
101 }
|
|
102 }
|
|
103
|
|
104 virtual bool Apply(const std::string& value) const
|
|
105 {
|
|
106 std::string tmp;
|
|
107 Toolbox::ToLowerCase(tmp, value);
|
|
108 return values_.find(tmp) != values_.end();
|
|
109 }
|
|
110 };
|
|
111
|
|
112
|
|
113 class DicomFindQuery::RangeConstraint : public DicomFindQuery::IConstraint
|
|
114 {
|
|
115 private:
|
|
116 std::string lower_;
|
|
117 std::string upper_;
|
|
118
|
|
119 public:
|
|
120 RangeConstraint(const std::string& range)
|
|
121 {
|
|
122 size_t separator = range.find('-');
|
|
123 Toolbox::ToLowerCase(lower_, range.substr(0, separator));
|
|
124 Toolbox::ToLowerCase(upper_, range.substr(separator + 1));
|
|
125 }
|
|
126
|
|
127 virtual bool Apply(const std::string& value) const
|
|
128 {
|
|
129 std::string v;
|
|
130 Toolbox::ToLowerCase(v, value);
|
|
131
|
|
132 if (lower_.size() == 0 &&
|
|
133 upper_.size() == 0)
|
|
134 {
|
|
135 return false;
|
|
136 }
|
|
137
|
|
138 if (lower_.size() == 0)
|
|
139 {
|
|
140 return v <= upper_;
|
|
141 }
|
|
142
|
|
143 if (upper_.size() == 0)
|
|
144 {
|
|
145 return v >= lower_;
|
|
146 }
|
|
147
|
|
148 return (v >= lower_ && v <= upper_);
|
|
149 }
|
|
150 };
|
|
151
|
|
152
|
|
153 class DicomFindQuery::WildcardConstraint : public DicomFindQuery::IConstraint
|
|
154 {
|
|
155 private:
|
|
156 boost::regex pattern_;
|
|
157
|
|
158 public:
|
|
159 WildcardConstraint(const std::string& wildcard)
|
|
160 {
|
|
161 pattern_ = boost::regex(Toolbox::WildcardToRegularExpression(wildcard),
|
|
162 boost::regex::icase /* case insensitive search */);
|
|
163 }
|
|
164
|
|
165 virtual bool Apply(const std::string& value) const
|
|
166 {
|
|
167 return boost::regex_match(value, pattern_);
|
|
168 }
|
|
169 };
|
|
170
|
|
171
|
|
172 void DicomFindQuery::PrepareMainDicomTags(ResourceType level)
|
|
173 {
|
|
174 std::set<DicomTag> tags;
|
|
175 DicomMap::GetMainDicomTags(tags, level);
|
|
176
|
|
177 for (std::set<DicomTag>::const_iterator
|
|
178 it = tags.begin(); it != tags.end(); ++it)
|
|
179 {
|
|
180 mainDicomTags_[*it] = level;
|
|
181 }
|
|
182 }
|
|
183
|
|
184
|
|
185 DicomFindQuery::DicomFindQuery() :
|
|
186 level_(ResourceType_Patient),
|
|
187 filterJson_(false)
|
|
188 {
|
|
189 PrepareMainDicomTags(ResourceType_Patient);
|
|
190 PrepareMainDicomTags(ResourceType_Study);
|
|
191 PrepareMainDicomTags(ResourceType_Series);
|
|
192 PrepareMainDicomTags(ResourceType_Instance);
|
|
193 }
|
|
194
|
|
195
|
|
196 DicomFindQuery::~DicomFindQuery()
|
|
197 {
|
|
198 for (Constraints::iterator it = constraints_.begin();
|
|
199 it != constraints_.end(); it++)
|
|
200 {
|
|
201 delete it->second;
|
|
202 }
|
|
203 }
|
|
204
|
|
205
|
|
206
|
|
207
|
|
208 void DicomFindQuery::AssignConstraint(const DicomTag& tag,
|
|
209 IConstraint* constraint)
|
|
210 {
|
|
211 Constraints::iterator it = constraints_.find(tag);
|
|
212
|
|
213 if (it != constraints_.end())
|
|
214 {
|
|
215 constraints_.erase(it);
|
|
216 }
|
|
217
|
|
218 constraints_[tag] = constraint;
|
|
219
|
|
220 MainDicomTags::const_iterator tmp = mainDicomTags_.find(tag);
|
|
221 if (tmp == mainDicomTags_.end())
|
|
222 {
|
|
223 // The query depends upon a DICOM tag that is not a main tag
|
|
224 // from the point of view of Orthanc, we need to decode the
|
|
225 // JSON file on the disk.
|
|
226 filterJson_ = true;
|
|
227 }
|
|
228 else
|
|
229 {
|
|
230 filteredLevels_.insert(tmp->second);
|
|
231 }
|
|
232 }
|
|
233
|
|
234
|
|
235 void DicomFindQuery::SetConstraint(const DicomTag& tag,
|
|
236 const std::string& constraint)
|
|
237 {
|
|
238 // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained
|
|
239 // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html
|
|
240
|
|
241 if (constraint.find('-') != std::string::npos)
|
|
242 {
|
|
243 AssignConstraint(tag, new RangeConstraint(constraint));
|
|
244 }
|
|
245 else if (constraint.find('\\') != std::string::npos)
|
|
246 {
|
|
247 AssignConstraint(tag, new ListConstraint(constraint));
|
|
248 }
|
|
249 else if (constraint.find('*') != std::string::npos ||
|
|
250 constraint.find('?') != std::string::npos)
|
|
251 {
|
|
252 AssignConstraint(tag, new WildcardConstraint(constraint));
|
|
253 }
|
|
254 else
|
|
255 {
|
|
256 /**
|
|
257 * Case-insensitive match for PN value representation (Patient
|
|
258 * Name). Case-senstive match for all the other value
|
|
259 * representations.
|
|
260 *
|
|
261 * Reference: DICOM PS 3.4
|
|
262 * - C.2.2.2.1 ("Single Value Matching")
|
|
263 * - C.2.2.2.4 ("Wild Card Matching")
|
|
264 * http://medical.nema.org/Dicom/2011/11_04pu.pdf
|
|
265 *
|
|
266 * "Except for Attributes with a PN Value Representation, only
|
|
267 * entities with values which match exactly the value specified in the
|
|
268 * request shall match. This matching is case-sensitive, i.e.,
|
|
269 * sensitive to the exact encoding of the key attribute value in
|
|
270 * character sets where a letter may have multiple encodings (e.g.,
|
|
271 * based on its case, its position in a word, or whether it is
|
|
272 * accented)
|
|
273 *
|
|
274 * For Attributes with a PN Value Representation (e.g., Patient Name
|
|
275 * (0010,0010)), an application may perform literal matching that is
|
|
276 * either case-sensitive, or that is insensitive to some or all
|
|
277 * aspects of case, position, accent, or other character encoding
|
|
278 * variants."
|
|
279 *
|
|
280 * (0008,0018) UI SOPInstanceUID => Case-sensitive
|
|
281 * (0008,0050) SH AccessionNumber => Case-sensitive
|
|
282 * (0010,0020) LO PatientID => Case-sensitive
|
|
283 * (0020,000D) UI StudyInstanceUID => Case-sensitive
|
|
284 * (0020,000E) UI SeriesInstanceUID => Case-sensitive
|
|
285 **/
|
|
286
|
|
287 AssignConstraint(tag, new ValueConstraint(constraint, FromDcmtkBridge::IsPNValueRepresentation(tag)));
|
|
288 }
|
|
289 }
|
|
290
|
|
291
|
|
292 bool DicomFindQuery::RestrictIdentifier(std::string& value,
|
|
293 DicomTag identifier) const
|
|
294 {
|
|
295 Constraints::const_iterator it = constraints_.find(identifier);
|
|
296 if (it == constraints_.end() ||
|
|
297 !it->second->IsExactConstraint())
|
|
298 {
|
|
299 return false;
|
|
300 }
|
|
301 else
|
|
302 {
|
|
303 value = dynamic_cast<ValueConstraint*>(it->second)->GetValue();
|
|
304 return true;
|
|
305 }
|
|
306 }
|
|
307
|
|
308 bool DicomFindQuery::HasMainDicomTagsFilter(ResourceType level) const
|
|
309 {
|
|
310 return filteredLevels_.find(level) != filteredLevels_.end();
|
|
311 }
|
|
312
|
|
313 bool DicomFindQuery::FilterMainDicomTags(const DicomMap& mainTags,
|
|
314 ResourceType level) const
|
|
315 {
|
|
316 std::set<DicomTag> tags;
|
|
317 mainTags.GetTags(tags);
|
|
318
|
|
319 for (std::set<DicomTag>::const_iterator
|
|
320 it = tags.begin(); it != tags.end(); ++it)
|
|
321 {
|
|
322 Constraints::const_iterator constraint = constraints_.find(*it);
|
|
323 if (!constraint->second->Apply(mainTags.GetValue(*it).AsString()))
|
|
324 {
|
|
325 return false;
|
|
326 }
|
|
327 }
|
|
328
|
|
329 return true;
|
|
330 }
|
|
331
|
|
332 bool DicomFindQuery::HasInstanceFilter() const
|
|
333 {
|
|
334 return filterJson_;
|
|
335 }
|
|
336
|
|
337 bool DicomFindQuery::FilterInstance(const std::string& instanceId,
|
|
338 const Json::Value& content) const
|
|
339 {
|
|
340 for (Constraints::const_iterator it = constraints_.begin();
|
|
341 it != constraints_.end(); ++it)
|
|
342 {
|
|
343 std::string tag = it->first.Format();
|
|
344 std::string value;
|
|
345 if (content.isMember(tag))
|
|
346 {
|
|
347 value = content.get(tag, Json::arrayValue).get("Value", "").asString();
|
|
348 }
|
|
349
|
|
350 if (!it->second->Apply(value))
|
|
351 {
|
|
352 return false;
|
|
353 }
|
|
354 }
|
|
355
|
|
356 return true;
|
|
357 }
|
|
358 }
|