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-2020 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 "DicomModalityStoreJob.h"
36 
37 #include "../../../OrthancFramework/Sources/Compatibility.h"
38 #include "../../../OrthancFramework/Sources/DicomNetworking/DicomAssociation.h"
39 #include "../../../OrthancFramework/Sources/Logging.h"
40 #include "../../../OrthancFramework/Sources/SerializationToolbox.h"
41 #include "../ServerContext.h"
42 #include "../StorageCommitmentReports.h"
43 
44 
45 namespace Orthanc
46 {
OpenConnection()47   void DicomModalityStoreJob::OpenConnection()
48   {
49     if (connection_.get() == NULL)
50     {
51       connection_.reset(new DicomStoreUserConnection(parameters_));
52     }
53   }
54 
55 
HandleInstance(const std::string & instance)56   bool DicomModalityStoreJob::HandleInstance(const std::string& instance)
57   {
58     assert(IsStarted());
59     OpenConnection();
60 
61     LOG(INFO) << "Sending instance " << instance << " to modality \""
62               << parameters_.GetRemoteModality().GetApplicationEntityTitle() << "\"";
63 
64     std::string dicom;
65 
66     try
67     {
68       context_.ReadDicom(dicom, instance);
69     }
70     catch (OrthancException& e)
71     {
72       LOG(WARNING) << "An instance was removed after the job was issued: " << instance;
73       return false;
74     }
75 
76     std::string sopClassUid, sopInstanceUid;
77     context_.StoreWithTranscoding(sopClassUid, sopInstanceUid, *connection_, dicom,
78                                   HasMoveOriginator(), moveOriginatorAet_, moveOriginatorId_);
79 
80     if (storageCommitment_)
81     {
82       sopClassUids_.push_back(sopClassUid);
83       sopInstanceUids_.push_back(sopInstanceUid);
84 
85       if (sopClassUids_.size() != sopInstanceUids_.size() ||
86           sopClassUids_.size() > GetInstancesCount())
87       {
88         throw OrthancException(ErrorCode_InternalError);
89       }
90 
91       if (sopClassUids_.size() == GetInstancesCount())
92       {
93         assert(IsStarted());
94         connection_.reset(NULL);
95 
96         const std::string& remoteAet = parameters_.GetRemoteModality().GetApplicationEntityTitle();
97 
98         LOG(INFO) << "Sending storage commitment request to modality: " << remoteAet;
99 
100         // Create a "pending" storage commitment report BEFORE the
101         // actual SCU call in order to avoid race conditions
102         context_.GetStorageCommitmentReports().Store(
103           transactionUid_, new StorageCommitmentReports::Report(remoteAet));
104 
105         std::vector<std::string> a(sopClassUids_.begin(), sopClassUids_.end());
106         std::vector<std::string> b(sopInstanceUids_.begin(), sopInstanceUids_.end());
107 
108         DicomAssociation::RequestStorageCommitment(parameters_, transactionUid_, a, b);
109       }
110     }
111 
112     //boost::this_thread::sleep(boost::posix_time::milliseconds(500));
113 
114     return true;
115   }
116 
117 
HandleTrailingStep()118   bool DicomModalityStoreJob::HandleTrailingStep()
119   {
120     throw OrthancException(ErrorCode_InternalError);
121   }
122 
123 
DicomModalityStoreJob(ServerContext & context)124   DicomModalityStoreJob::DicomModalityStoreJob(ServerContext& context) :
125     context_(context),
126     moveOriginatorId_(0),      // By default, not a C-MOVE
127     storageCommitment_(false)  // By default, no storage commitment
128   {
129     ResetStorageCommitment();
130   }
131 
132 
SetLocalAet(const std::string & aet)133   void DicomModalityStoreJob::SetLocalAet(const std::string& aet)
134   {
135     if (IsStarted())
136     {
137       throw OrthancException(ErrorCode_BadSequenceOfCalls);
138     }
139     else
140     {
141       parameters_.SetLocalApplicationEntityTitle(aet);
142     }
143   }
144 
145 
SetRemoteModality(const RemoteModalityParameters & remote)146   void DicomModalityStoreJob::SetRemoteModality(const RemoteModalityParameters& remote)
147   {
148     if (IsStarted())
149     {
150       throw OrthancException(ErrorCode_BadSequenceOfCalls);
151     }
152     else
153     {
154       parameters_.SetRemoteModality(remote);
155     }
156   }
157 
158 
SetTimeout(uint32_t seconds)159   void DicomModalityStoreJob::SetTimeout(uint32_t seconds)
160   {
161     if (IsStarted())
162     {
163       throw OrthancException(ErrorCode_BadSequenceOfCalls);
164     }
165     else
166     {
167       parameters_.SetTimeout(seconds);
168     }
169   }
170 
171 
GetMoveOriginatorAet() const172   const std::string& DicomModalityStoreJob::GetMoveOriginatorAet() const
173   {
174     if (HasMoveOriginator())
175     {
176       return moveOriginatorAet_;
177     }
178     else
179     {
180       throw OrthancException(ErrorCode_BadSequenceOfCalls);
181     }
182   }
183 
184 
GetMoveOriginatorId() const185   uint16_t DicomModalityStoreJob::GetMoveOriginatorId() const
186   {
187     if (HasMoveOriginator())
188     {
189       return moveOriginatorId_;
190     }
191     else
192     {
193       throw OrthancException(ErrorCode_BadSequenceOfCalls);
194     }
195   }
196 
197 
SetMoveOriginator(const std::string & aet,int id)198   void DicomModalityStoreJob::SetMoveOriginator(const std::string& aet,
199                                                 int id)
200   {
201     if (IsStarted())
202     {
203       throw OrthancException(ErrorCode_BadSequenceOfCalls);
204     }
205     else if (id < 0 ||
206              id >= 65536)
207     {
208       throw OrthancException(ErrorCode_ParameterOutOfRange);
209     }
210     else
211     {
212       moveOriginatorId_ = static_cast<uint16_t>(id);
213       moveOriginatorAet_ = aet;
214     }
215   }
216 
Stop(JobStopReason reason)217   void DicomModalityStoreJob::Stop(JobStopReason reason)   // For pausing jobs
218   {
219     connection_.reset(NULL);
220   }
221 
222 
ResetStorageCommitment()223   void DicomModalityStoreJob::ResetStorageCommitment()
224   {
225     if (storageCommitment_)
226     {
227       transactionUid_ = Toolbox::GenerateDicomPrivateUniqueIdentifier();
228       sopClassUids_.clear();
229       sopInstanceUids_.clear();
230     }
231   }
232 
233 
Reset()234   void DicomModalityStoreJob::Reset()
235   {
236     SetOfInstancesJob::Reset();
237 
238     /**
239      * "After the N-EVENT-REPORT has been sent, the Transaction UID is
240      * no longer active and shall not be reused for other
241      * transactions." => Need to reset the transaction UID here
242      * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html
243      **/
244     ResetStorageCommitment();
245   }
246 
247 
EnableStorageCommitment(bool enabled)248   void DicomModalityStoreJob::EnableStorageCommitment(bool enabled)
249   {
250     storageCommitment_ = enabled;
251     ResetStorageCommitment();
252   }
253 
254 
GetPublicContent(Json::Value & value)255   void DicomModalityStoreJob::GetPublicContent(Json::Value& value)
256   {
257     SetOfInstancesJob::GetPublicContent(value);
258 
259     value["LocalAet"] = parameters_.GetLocalApplicationEntityTitle();
260     value["RemoteAet"] = parameters_.GetRemoteModality().GetApplicationEntityTitle();
261 
262     if (HasMoveOriginator())
263     {
264       value["MoveOriginatorAET"] = GetMoveOriginatorAet();
265       value["MoveOriginatorID"] = GetMoveOriginatorId();
266     }
267 
268     if (storageCommitment_)
269     {
270       value["StorageCommitmentTransactionUID"] = transactionUid_;
271     }
272   }
273 
274 
275   static const char* MOVE_ORIGINATOR_AET = "MoveOriginatorAet";
276   static const char* MOVE_ORIGINATOR_ID = "MoveOriginatorId";
277   static const char* STORAGE_COMMITMENT = "StorageCommitment";
278 
279 
DicomModalityStoreJob(ServerContext & context,const Json::Value & serialized)280   DicomModalityStoreJob::DicomModalityStoreJob(ServerContext& context,
281                                                const Json::Value& serialized) :
282     SetOfInstancesJob(serialized),
283     context_(context)
284   {
285     moveOriginatorAet_ = SerializationToolbox::ReadString(serialized, MOVE_ORIGINATOR_AET);
286     moveOriginatorId_ = static_cast<uint16_t>
287       (SerializationToolbox::ReadUnsignedInteger(serialized, MOVE_ORIGINATOR_ID));
288     EnableStorageCommitment(SerializationToolbox::ReadBoolean(serialized, STORAGE_COMMITMENT));
289 
290     parameters_ = DicomAssociationParameters::UnserializeJob(serialized);
291   }
292 
293 
Serialize(Json::Value & target)294   bool DicomModalityStoreJob::Serialize(Json::Value& target)
295   {
296     if (!SetOfInstancesJob::Serialize(target))
297     {
298       return false;
299     }
300     else
301     {
302       parameters_.SerializeJob(target);
303       target[MOVE_ORIGINATOR_AET] = moveOriginatorAet_;
304       target[MOVE_ORIGINATOR_ID] = moveOriginatorId_;
305       target[STORAGE_COMMITMENT] = storageCommitment_;
306       return true;
307     }
308   }
309 }
310