3818
|
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-2019 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 #include "PrecompiledHeadersServer.h"
|
|
34 #include "OrthancGetRequestHandler.h"
|
|
35
|
|
36 #include <dcmtk/dcmnet/assoc.h>
|
|
37 #include <dcmtk/dcmnet/dimse.h>
|
|
38 #include <dcmtk/dcmdata/dcdeftag.h>
|
|
39 #include <dcmtk/dcmdata/dcistrmb.h>
|
|
40 #include <dcmtk/dcmnet/diutil.h>
|
|
41
|
|
42 #include "../../Core/DicomParsing/FromDcmtkBridge.h"
|
|
43 #include "../Core/DicomFormat/DicomArray.h"
|
|
44 #include "../Core/Logging.h"
|
|
45 #include "../Core/MetricsRegistry.h"
|
|
46 #include "OrthancConfiguration.h"
|
|
47 #include "ServerContext.h"
|
|
48 #include "ServerJobs/DicomModalityStoreJob.h"
|
|
49
|
|
50
|
|
51
|
|
52 namespace Orthanc
|
|
53 {
|
|
54 namespace
|
|
55 {
|
|
56 // Anonymous namespace to avoid clashes between compilation modules
|
|
57
|
|
58 static void getSubOpProgressCallback(void * /* callbackData */,
|
|
59 T_DIMSE_StoreProgress *progress,
|
|
60 T_DIMSE_C_StoreRQ * /*req*/)
|
|
61 {
|
|
62 // SBL - no logging to be done here.
|
|
63 }
|
|
64 }
|
|
65
|
|
66 OrthancGetRequestHandler::Status OrthancGetRequestHandler::DoNext(T_ASC_Association* assoc)
|
|
67 {
|
|
68 if (position_ >= instances_.size())
|
|
69 {
|
|
70 return Status_Failure;
|
|
71 }
|
|
72
|
|
73 const std::string& id = instances_[position_++];
|
|
74
|
|
75 std::string dicom;
|
|
76 context_.ReadDicom(dicom, id);
|
|
77
|
|
78 if(dicom.size() <= 0)
|
|
79 {
|
|
80 return Status_Failure;
|
|
81 }
|
|
82
|
|
83 DcmInputBufferStream is;
|
|
84 is.setBuffer(&dicom[0], dicom.size());
|
|
85 is.setEos();
|
|
86
|
|
87 DcmFileFormat dcmff;
|
|
88 OFCondition cond = dcmff.read(is, EXS_Unknown, EGL_noChange, DCM_MaxReadLength);
|
|
89 if(cond.bad())
|
|
90 {
|
|
91 return Status_Failure;
|
|
92 }
|
|
93
|
|
94 // Determine the storage SOP class UID for this instance
|
|
95 DIC_UI sopClass;
|
|
96 DIC_UI sopInstance;
|
|
97
|
|
98 #if DCMTK_VERSION_NUMBER >= 364
|
|
99 if (!DU_findSOPClassAndInstanceInDataSet(static_cast<DcmItem *> (dcmff.getDataset()),
|
|
100 sopClass, sizeof(sopClass),
|
|
101 sopInstance, sizeof(sopInstance)))
|
|
102 #else
|
3859
|
103 if (!DU_findSOPClassAndInstanceInDataSet(dcmff.getDataset(), sopClass, sopInstance))
|
3818
|
104 #endif
|
|
105 {
|
|
106 throw OrthancException(ErrorCode_NoSopClassOrInstance,
|
|
107 "Unable to determine the SOP class/instance for C-STORE with AET " +
|
|
108 originatorAet_);
|
|
109 }
|
|
110
|
|
111
|
|
112
|
|
113
|
|
114 cond = performGetSubOp(assoc, sopClass, sopInstance, dcmff.getDataset());
|
|
115 if (getCancelled_) {
|
|
116 LOG (INFO) << "Get SCP: Received C-Cancel RQ";
|
|
117 }
|
|
118
|
|
119 if(cond.bad() || getCancelled_)
|
|
120 {
|
|
121 return Status_Failure;
|
|
122 }
|
|
123
|
|
124 return Status_Success;
|
|
125 }
|
|
126
|
|
127 void OrthancGetRequestHandler::addFailedUIDInstance(const char *sopInstance)
|
|
128 {
|
|
129 size_t len;
|
|
130
|
|
131 if (failedUIDs_ == NULL) {
|
|
132 if ((failedUIDs_ = (char*)malloc(DIC_UI_LEN+1)) == NULL) {
|
|
133 LOG (ERROR) << "malloc failure: addFailedUIDInstance";
|
|
134 return;
|
|
135 }
|
|
136 strcpy(failedUIDs_, sopInstance);
|
|
137 } else {
|
|
138 len = strlen(failedUIDs_);
|
|
139 if ((failedUIDs_ = (char*)realloc(failedUIDs_,
|
|
140 (len+strlen(sopInstance)+2))) == NULL) {
|
|
141 LOG (ERROR) << "realloc failure: addFailedUIDInstance";
|
|
142 return;
|
|
143 }
|
|
144 // tag sopInstance onto end of old with '\' between
|
|
145 strcat(failedUIDs_, "\\");
|
|
146 strcat(failedUIDs_, sopInstance);
|
|
147 }
|
|
148 }
|
|
149
|
|
150
|
|
151 OFCondition OrthancGetRequestHandler::performGetSubOp(T_ASC_Association* assoc,
|
|
152 DIC_UI sopClass,
|
|
153 DIC_UI sopInstance,
|
|
154 DcmDataset *dataset)
|
|
155 {
|
|
156 OFCondition cond = EC_Normal;
|
|
157 T_DIMSE_C_StoreRQ req;
|
|
158 T_DIMSE_C_StoreRSP rsp;
|
|
159 DIC_US msgId;
|
|
160 T_ASC_PresentationContextID presId;
|
|
161 DcmDataset *stDetail = NULL;
|
|
162
|
|
163
|
|
164 msgId = assoc->nextMsgID++;
|
|
165
|
|
166 // which presentation context should be used
|
|
167 presId = ASC_findAcceptedPresentationContextID(assoc,
|
|
168 sopClass);
|
|
169 if (presId == 0) {
|
|
170 nFailed_++;
|
|
171 addFailedUIDInstance(sopInstance);
|
|
172 LOG (ERROR) << "Get SCP: storeSCU: No presentation context for: ("
|
|
173 << dcmSOPClassUIDToModality(sopClass, "OT") << ") " << sopClass;
|
|
174 return DIMSE_NOVALIDPRESENTATIONCONTEXTID;
|
|
175 } else {
|
|
176 // make sure that we can send images in this presentation context
|
|
177 T_ASC_PresentationContext pc;
|
|
178 ASC_findAcceptedPresentationContext(assoc->params, presId, &pc);
|
|
179 // the acceptedRole is the association requestor role
|
|
180 if ((pc.acceptedRole != ASC_SC_ROLE_SCP) && (pc.acceptedRole != ASC_SC_ROLE_SCUSCP))
|
|
181 {
|
|
182 // the role is not appropriate
|
|
183 nFailed_++;
|
|
184 addFailedUIDInstance(sopInstance);
|
|
185 LOG (ERROR) <<"Get SCP: storeSCU: [No presentation context with requestor SCP role for: ("
|
|
186 << dcmSOPClassUIDToModality(sopClass, "OT") << ") " << sopClass;
|
|
187 return DIMSE_NOVALIDPRESENTATIONCONTEXTID;
|
|
188 }
|
|
189 }
|
|
190
|
|
191 req.MessageID = msgId;
|
|
192 strcpy(req.AffectedSOPClassUID, sopClass);
|
|
193 strcpy(req.AffectedSOPInstanceUID, sopInstance);
|
|
194 req.DataSetType = DIMSE_DATASET_PRESENT;
|
|
195 req.Priority = priority_;
|
|
196 req.opts = 0;
|
|
197
|
|
198 LOG (INFO) << "Store SCU RQ: MsgID " << msgId << ", ("
|
|
199 << dcmSOPClassUIDToModality(sopClass, "OT") << ")";
|
|
200
|
|
201 T_DIMSE_DetectedCancelParameters cancelParameters;
|
|
202
|
|
203 cond = DIMSE_storeUser(assoc, presId, &req,
|
|
204 NULL, dataset, getSubOpProgressCallback, this, DIMSE_BLOCKING, 0,
|
|
205 &rsp, &stDetail, &cancelParameters);
|
|
206
|
|
207 if (cond.good()) {
|
|
208 if (cancelParameters.cancelEncountered) {
|
|
209 if (origPresId == cancelParameters.presId &&
|
|
210 origMsgId == cancelParameters.req.MessageIDBeingRespondedTo) {
|
|
211 getCancelled_ = OFTrue;
|
|
212 } else {
|
|
213 LOG (ERROR) << "Get SCP: Unexpected C-Cancel-RQ encountered: pid=" << (int)cancelParameters.presId
|
|
214 << ", mid=" << (int)cancelParameters.req.MessageIDBeingRespondedTo;
|
|
215 }
|
|
216 }
|
|
217
|
|
218 if (rsp.DimseStatus == STATUS_Success) {
|
|
219 // everything ok
|
|
220 nCompleted_++;
|
|
221 } else if ((rsp.DimseStatus & 0xf000) == 0xb000) {
|
|
222 // a warning status message
|
|
223 warningCount_++;
|
|
224 LOG (ERROR) << "Get SCP: Store Warning: Response Status: " << DU_cstoreStatusString(rsp.DimseStatus);
|
|
225 } else {
|
|
226 nFailed_++;
|
|
227 addFailedUIDInstance(sopInstance);
|
|
228 // print a status message
|
|
229 LOG (ERROR) << "Get SCP: Store Failed: Response Status: "
|
|
230 << DU_cstoreStatusString(rsp.DimseStatus);
|
|
231 }
|
|
232 } else {
|
|
233 nFailed_++;
|
|
234 addFailedUIDInstance(sopInstance);
|
|
235 OFString temp_str;
|
|
236 LOG (ERROR) << "Get SCP: storeSCU: Store Request Failed: " << DimseCondition::dump(temp_str, cond);
|
|
237 }
|
|
238 if (stDetail) {
|
|
239 LOG (INFO) << " Status Detail:" << OFendl << DcmObject::PrintHelper(*stDetail);
|
|
240 delete stDetail;
|
|
241 }
|
|
242 return cond;
|
|
243 }
|
|
244
|
|
245 bool OrthancGetRequestHandler::LookupIdentifiers(std::vector<std::string>& publicIds,
|
|
246 ResourceType level,
|
|
247 const DicomMap& input)
|
|
248 {
|
|
249 DicomTag tag(0, 0); // Dummy initialization
|
|
250
|
|
251 switch (level)
|
|
252 {
|
|
253 case ResourceType_Patient:
|
|
254 tag = DICOM_TAG_PATIENT_ID;
|
|
255 break;
|
|
256
|
|
257 case ResourceType_Study:
|
|
258 tag = (input.HasTag(DICOM_TAG_ACCESSION_NUMBER) ?
|
|
259 DICOM_TAG_ACCESSION_NUMBER : DICOM_TAG_STUDY_INSTANCE_UID);
|
|
260 break;
|
|
261
|
|
262 case ResourceType_Series:
|
|
263 tag = DICOM_TAG_SERIES_INSTANCE_UID;
|
|
264 break;
|
|
265
|
|
266 case ResourceType_Instance:
|
|
267 tag = DICOM_TAG_SOP_INSTANCE_UID;
|
|
268 break;
|
|
269
|
|
270 default:
|
|
271 throw OrthancException(ErrorCode_ParameterOutOfRange);
|
|
272 }
|
|
273
|
|
274 if (!input.HasTag(tag))
|
|
275 {
|
|
276 return false;
|
|
277 }
|
|
278
|
|
279 const DicomValue& value = input.GetValue(tag);
|
|
280 if (value.IsNull() ||
|
|
281 value.IsBinary())
|
|
282 {
|
|
283 return false;
|
|
284 }
|
|
285 else
|
|
286 {
|
|
287 const std::string& content = value.GetContent();
|
|
288 context_.GetIndex().LookupIdentifierExact(publicIds, level, tag, content);
|
|
289 return true;
|
|
290 }
|
|
291 }
|
|
292
|
|
293
|
|
294 bool OrthancGetRequestHandler::Handle(const DicomMap& input,
|
|
295 const std::string& originatorIp,
|
|
296 const std::string& originatorAet,
|
|
297 const std::string& calledAet)
|
|
298 {
|
|
299 MetricsRegistry::Timer timer(context_.GetMetricsRegistry(), "orthanc_get_scp_duration_ms");
|
|
300
|
|
301 LOG(WARNING) << "Get-SCU request received from AET \"" << originatorAet << "\"";
|
|
302
|
|
303 {
|
|
304 DicomArray query(input);
|
|
305 for (size_t i = 0; i < query.GetSize(); i++)
|
|
306 {
|
|
307 if (!query.GetElement(i).GetValue().IsNull())
|
|
308 {
|
|
309 LOG(INFO) << " " << query.GetElement(i).GetTag()
|
|
310 << " " << FromDcmtkBridge::GetTagName(query.GetElement(i))
|
|
311 << " = " << query.GetElement(i).GetValue().GetContent();
|
|
312 }
|
|
313 }
|
|
314 }
|
|
315
|
|
316 /**
|
|
317 * Retrieve the query level.
|
|
318 **/
|
|
319
|
|
320 const DicomValue* levelTmp = input.TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL);
|
|
321
|
|
322 assert(levelTmp != NULL);
|
|
323 ResourceType level = StringToResourceType(levelTmp->GetContent().c_str());
|
|
324
|
|
325
|
|
326 /**
|
|
327 * Lookup for the resource to be sent.
|
|
328 **/
|
|
329
|
|
330 std::vector<std::string> publicIds;
|
|
331
|
|
332 bool retVal = LookupIdentifiers(publicIds, level, input);
|
|
333 localAet_ = context_.GetDefaultLocalApplicationEntityTitle();
|
|
334 position_ = 0;
|
|
335 originatorAet_ = originatorAet;
|
|
336
|
|
337 {
|
|
338 OrthancConfiguration::ReaderLock lock;
|
|
339 remote_ = lock.GetConfiguration().GetModalityUsingAet(originatorAet);
|
|
340 }
|
|
341
|
|
342 for (size_t i = 0; i < publicIds.size(); i++)
|
|
343 {
|
|
344 LOG(INFO) << "Sending resource " << publicIds[i] << " to modality \""
|
|
345 << originatorAet << "\" in synchronous mode";
|
|
346
|
|
347 std::list<std::string> tmp;
|
|
348 context_.GetIndex().GetChildInstances(tmp, publicIds[i]);
|
|
349
|
|
350 instances_.reserve(tmp.size());
|
|
351 for (std::list<std::string>::iterator it = tmp.begin(); it != tmp.end(); ++it)
|
|
352 {
|
|
353 instances_.push_back(*it);
|
|
354 }
|
|
355 }
|
|
356 failedUIDs_ = NULL;
|
|
357 getCancelled_ = OFFalse;
|
|
358
|
|
359 nRemaining_ = GetSubOperationCount();
|
|
360 nCompleted_ = 0;
|
|
361 nFailed_ = 0;
|
|
362 warningCount_ = 0;
|
|
363
|
|
364 return retVal;
|
|
365
|
|
366
|
|
367 }
|
|
368 };
|