fw4spl
ioDicomWeb/src/ioDicomWeb/SSeriesPuller.cpp
1 /* ***** BEGIN LICENSE BLOCK *****
2  * FW4SPL - Copyright (C) IRCAD, 2018.
3  * Distributed under the terms of the GNU Lesser General Public License (LGPL) as
4  * published by the Free Software Foundation.
5  * ****** END LICENSE BLOCK ****** */
6 
7 #include "ioDicomWeb/SSeriesPuller.hpp"
8 
9 #include <fwCom/Signal.hpp>
10 #include <fwCom/Signal.hxx>
11 #include <fwCom/Slots.hpp>
12 #include <fwCom/Slots.hxx>
13 
14 #include <fwData/Vector.hpp>
15 
16 #include <fwGui/dialog/MessageDialog.hpp>
17 #include <fwGui/dialog/ProgressDialog.hpp>
18 
19 #include <fwMedData/DicomSeries.hpp>
20 
21 #include <fwMedDataTools/helper/SeriesDB.hpp>
22 
23 #include <fwNetworkIO/exceptions/Base.hpp>
24 #include <fwNetworkIO/helper/Series.hpp>
25 #include <fwNetworkIO/http/Request.hpp>
26 
27 #include <fwPreferences/helper.hpp>
28 
29 #include <fwServices/registry/ObjectService.hpp>
30 #include <fwServices/registry/ServiceConfig.hpp>
31 #include <fwServices/registry/ServiceFactory.hpp>
32 
33 #include <fwTools/System.hpp>
34 
35 #include <boost/filesystem/operations.hpp>
36 
37 namespace ioDicomWeb
38 {
39 
40 //------------------------------------------------------------------------------
41 
43  m_isPulling(false),
44  m_seriesIndex(0)
45 {
46 }
47 
48 //------------------------------------------------------------------------------
49 
51 {
52 }
53 
54 //------------------------------------------------------------------------------
55 
57 {
58  ::fwRuntime::ConfigurationElement::sptr config = m_configuration->findConfigurationElement("config");
59  SLM_ASSERT("The service ::ioDicomWeb::SSeriesPuller must have a \"config\" element.", config);
60 
61  bool success;
62 
63  // Dicom Reader
64  ::boost::tie(success, m_dicomReaderType) = config->getSafeAttributeValue("dicomReader");
65  SLM_ASSERT("It should be a \"dicomReader\" in the ::ioDicomWeb::SSeriesPuller config element.", success);
66 
67  // Dicom Reader Config
68  ::boost::tie(success, m_dicomReaderSrvConfig) = config->getSafeAttributeValue("dicomReaderConfig");
69 
70  ::fwServices::IService::ConfigType configuration = this->getConfigTree();
71  //Parse server port and hostname
72  if(configuration.count("server"))
73  {
74  const std::string serverInfo = configuration.get("server", "");
75  const std::string::size_type splitPosition = serverInfo.find(':');
76  SLM_ASSERT("Server info not formatted correctly", splitPosition != std::string::npos);
77 
78  const std::string hostnameStr = serverInfo.substr(0, splitPosition);
79  const std::string portStr = serverInfo.substr(splitPosition + 1, serverInfo.size());
80 
81  m_serverHostnameKey = this->getPreferenceKey(hostnameStr);
82  m_serverPortKey = this->getPreferenceKey(portStr);
83 
84  if(m_serverHostnameKey.empty())
85  {
86  m_serverHostname = hostnameStr;
87  }
88  if(m_serverPortKey.empty())
89  {
90  m_serverPort = std::stoi(portStr);
91  }
92  }
93  else
94  {
95  throw ::fwTools::Failed("'server' element not found");
96  }
97 
98 }
99 
100 // -----------------------------------------------------------------------------
101 
102 std::string SSeriesPuller::getPreferenceKey(const std::string& key) const
103 {
104  std::string keyResult;
105  const size_t first = key.find('%');
106  const size_t last = key.rfind('%');
107  if (first == 0 && last == key.size() - 1)
108  {
109  keyResult = key.substr(1, key.size() - 2);
110  }
111  return keyResult;
112 }
113 
114 //------------------------------------------------------------------------------
115 
117 {
118  // Get Destination SeriesDB
119  m_destinationSeriesDB = this->getInOut< ::fwMedData::SeriesDB>("seriesDB");
120  SLM_ASSERT("The 'seriesDB' key doesn't exist.", m_destinationSeriesDB);
121 
122  // Create temporary SeriesDB
123  m_tempSeriesDB = ::fwMedData::SeriesDB::New();
124 
125  // Create reader
126  ::fwServices::registry::ServiceFactory::sptr srvFactory = ::fwServices::registry::ServiceFactory::getDefault();
127  m_dicomReader =
128  ::fwIO::IReader::dynamicCast(srvFactory->create(m_dicomReaderType));
129  SLM_ASSERT("Unable to create a reader of type: \"" + m_dicomReaderType + "\" in ::ioDicomWeb::SSeriesPuller.",
130  m_dicomReader);
131  ::fwServices::OSR::registerService(m_tempSeriesDB, ::fwIO::s_DATA_KEY,
132  ::fwServices::IService::AccessType::INOUT, m_dicomReader);
133 
134  if(!m_dicomReaderSrvConfig.empty())
135  {
136  // Get the config
137  ::fwRuntime::ConfigurationElement::csptr readerConfig =
139  m_dicomReaderSrvConfig, "::fwIO::IReader");
140 
141  SLM_ASSERT("Sorry, there is no service configuration "
142  << m_dicomReaderSrvConfig
143  << " for ::fwIO::IReader", readerConfig);
144 
145  m_dicomReader->setConfiguration( ::fwRuntime::ConfigurationElement::constCast(readerConfig) );
146  }
147 
148  m_dicomReader->configure();
149  m_dicomReader->start();
150 }
151 
152 //------------------------------------------------------------------------------
153 
155 {
156  // Stop reader service
157  m_dicomReader->stop();
158  ::fwServices::OSR::unregisterService(m_dicomReader);
159 }
160 
161 //------------------------------------------------------------------------------
162 
164 {
165  if(!m_serverHostnameKey.empty())
166  {
167  const std::string hostname = ::fwPreferences::getPreference(m_serverHostnameKey);
168  if(!hostname.empty())
169  {
170  m_serverHostname = hostname;
171  }
172  }
173  if(!m_serverPortKey.empty())
174  {
175  const std::string port = ::fwPreferences::getPreference(m_serverPortKey);
176  if(!port.empty())
177  {
178  m_serverPort = std::stoi(port);
179  }
180  }
181 
182  ::fwData::Vector::csptr selectedSeries = this->getInput< ::fwData::Vector >("selectedSeries");
183 
184  if(m_isPulling)
185  {
186  // Display a message to inform the user that the service is already pulling data.
188  messageBox.setTitle("Pulling Series");
189  messageBox.setMessage( "The service is already pulling data. Please wait until the pulling is done "
190  "before sending a new pull request." );
191  messageBox.setIcon(::fwGui::dialog::IMessageDialog::INFO);
192  messageBox.addButton(::fwGui::dialog::IMessageDialog::OK);
193  messageBox.show();
194  }
195  else if(selectedSeries->empty())
196  {
197  // Display a message to inform the user that there is no series selected.
199  messageBox.setTitle("Pulling Series");
200  messageBox.setMessage( "Unable to pull series, there is no series selected. " );
201  messageBox.setIcon(::fwGui::dialog::IMessageDialog::INFO);
202  messageBox.addButton(::fwGui::dialog::IMessageDialog::OK);
203  messageBox.show();
204  }
205  else
206  {
207  this->pullSeries();
208  }
209 }
210 
211 //------------------------------------------------------------------------------
212 
213 void SSeriesPuller::pullSeries()
214 {
215  // Catch any errors
216  try
217  {
218  // Clear map of Dicom series being pulled
219  m_pullingDicomSeriesMap.clear();
220 
221  // Set pulling boolean to true
222  m_isPulling = true;
223 
224  // Reset Counters
225  m_seriesIndex = 0;
226  m_instanceCount = 0;
227 
228  ::fwData::Vector::csptr selectedSeries = this->getInput< ::fwData::Vector >("selectedSeries");
229 
230  // Find which selected series must be pulled
231  DicomSeriesContainerType pullSeriesVector;
232  DicomSeriesContainerType selectedSeriesVector;
233 
234  ::fwData::Vector::ConstIteratorType it = selectedSeries->begin();
235  for(; it != selectedSeries->end(); ++it)
236  {
237  ::fwMedData::DicomSeries::sptr series = ::fwMedData::DicomSeries::dynamicCast(*it);
238 
239  // Check if the series must be pulled
240  if(series &&
241  std::find(m_localSeries.begin(), m_localSeries.end(), series->getInstanceUID()) == m_localSeries.end())
242  {
243  // Add series in the pulling series map
244  m_pullingDicomSeriesMap[series->getInstanceUID()] = series;
245 
246  pullSeriesVector.push_back(series);
247  m_instanceCount += series->getNumberOfInstances();
248  }
249  selectedSeriesVector.push_back(series);
250  }
251 
252  // Pull series
253  if(!pullSeriesVector.empty())
254  {
256  const InstanceUIDContainerType& seriesInstancesUIDs =
258  for( const std::string& seriesInstancesUID : seriesInstancesUIDs )
259  {
260  // Find Series according to SeriesInstanceUID
261  QJsonObject query;
262  query.insert("SeriesInstanceUID", seriesInstancesUID.c_str());
263 
264  QJsonObject body;
265  body.insert("Level", "Series");
266  body.insert("Query", query);
267  body.insert("Limit", 0);
268 
270  const std::string pacsServer("http://" + m_serverHostname + ":" + std::to_string(m_serverPort));
271 
273  ::fwNetworkIO::http::Request::sptr request = ::fwNetworkIO::http::Request::New(
274  pacsServer + "/tools/find");
275  QByteArray seriesAnswer;
276  try
277  {
278  seriesAnswer = m_clientQt.post(request, QJsonDocument(body).toJson());
279  }
280  catch (::fwNetworkIO::exceptions::HostNotFound& exception)
281  {
282  std::stringstream ss;
283  ss << "Host not found:\n"
284  << " Please check your configuration: \n"
285  << "Pacs host name: " << m_serverHostname << "\n"
286  << "Pacs port: " << m_serverPort << "\n";
287 
288  this->displayErrorMessage(ss.str());
289  SLM_WARN(exception.what());
290  }
291 
292  QJsonDocument jsonResponse = QJsonDocument::fromJson(seriesAnswer);
293  const QJsonArray& seriesArray = jsonResponse.array();
294 
295  const size_t seriesArraySize = seriesArray.count();
296  for(size_t i = 0; i < seriesArraySize; ++i)
297  {
298  const std::string& seriesUID = seriesArray.at(i).toString().toStdString();
299 
301  const std::string& instancesUrl(pacsServer + "/series/" + seriesUID);
302  const QByteArray& instancesAnswer =
303  m_clientQt.get( ::fwNetworkIO::http::Request::New(instancesUrl));
304  jsonResponse = QJsonDocument::fromJson(instancesAnswer);
305  const QJsonObject& jsonObj = jsonResponse.object();
306  const QJsonArray& instancesArray = jsonObj["Instances"].toArray();
307 
308  const size_t instancesArraySize = instancesArray.count();
309  for(size_t j = 0; j < instancesArraySize; ++j)
310  {
311  const std::string& instanceUID = instancesArray.at(j).toString().toStdString();
312 
314  const std::string instanceUrl(pacsServer +"/instances/" + instanceUID + "/file");
315 
316  try
317  {
318  m_path = m_clientQt.getFile(::fwNetworkIO::http::Request::New(instanceUrl));
319  }
320  catch (::fwNetworkIO::exceptions::ContentNotFound& exception)
321  {
322  std::stringstream ss;
323  ss << "Content not found: \n"
324  << "Unable download the DICOM instance. \n";
325 
326  this->displayErrorMessage(ss.str());
327  SLM_WARN(exception.what());
328  }
329 
330  // Create dicom folder
331  ::boost::filesystem::path instancePath = m_path.parent_path() / seriesInstancesUID;
332  QDir().mkpath(instancePath.string().c_str());
333  // Move dicom file to the created dicom folder
334  instancePath /= m_path.filename();
335  QFile().rename(m_path.string().c_str(), instancePath.string().c_str());
336  m_path = m_path.parent_path() / seriesInstancesUID;
337  }
338  }
339  }
340  }
341 
342  // Read series if there is no error
343  if(m_isPulling)
344  {
345  this->readLocalSeries(selectedSeriesVector);
346  }
347 
348  // Set pulling boolean to false
349  m_isPulling = false;
350 
351  }
352  catch (::fwNetworkIO::exceptions::Base& exception)
353  {
354  std::stringstream ss;
355  ss << "Unknown error.";
356  this->displayErrorMessage(ss.str());
357  SLM_WARN(exception.what());
358  m_isPulling = false;
359  }
360 }
361 
362 //------------------------------------------------------------------------------
363 
364 void SSeriesPuller::readLocalSeries(DicomSeriesContainerType selectedSeries)
365 {
366  // Read only series that are not in the SeriesDB
367  const InstanceUIDContainerType& alreadyLoadedSeries =
368  ::fwNetworkIO::helper::Series::toSeriesInstanceUIDContainer(m_destinationSeriesDB->getContainer());
369 
370  // Create temporary series helper
371  ::fwMedDataTools::helper::SeriesDB tempSDBhelper(m_tempSeriesDB);
372 
373  for(const ::fwMedData::Series::sptr& series: selectedSeries)
374  {
375  const std::string& selectedSeriesUID = series->getInstanceUID();
376 
377  // Add the series to the local series vector
378  if(std::find(m_localSeries.begin(), m_localSeries.end(), selectedSeriesUID) == m_localSeries.end())
379  {
380  m_localSeries.push_back(selectedSeriesUID);
381  }
382 
383  // Check if the series is loaded
384  if(std::find(alreadyLoadedSeries.begin(), alreadyLoadedSeries.end(),
385  selectedSeriesUID) == alreadyLoadedSeries.end())
386  {
387  // Clear temporary series
388  tempSDBhelper.clear();
389 
390  m_dicomReader->setFolder(m_path);
391  m_dicomReader->update();
392 
393  // Merge series
394  ::fwMedDataTools::helper::SeriesDB sDBhelper(m_destinationSeriesDB);
395  sDBhelper.merge(m_tempSeriesDB);
396  sDBhelper.notify();
397  }
398  }
399 }
400 
401 //------------------------------------------------------------------------------
402 
403 void SSeriesPuller::displayErrorMessage(const std::string& message) const
404 {
405  SLM_WARN("Error: " + message);
407  messageBox.setTitle("Error");
408  messageBox.setMessage( message );
409  messageBox.setIcon(::fwGui::dialog::IMessageDialog::CRITICAL);
410  messageBox.addButton(::fwGui::dialog::IMessageDialog::OK);
411  messageBox.show();
412 }
413 
414 //------------------------------------------------------------------------------
415 
416 } // namespace ioDicomWeb
virtual IODICOMWEB_API void stopping() override
Stops the DICOM reader.
FWNETWORKIO_API QByteArray post(Request::sptr request, const QByteArray &body)
Performs POST request.
Definition: ClientQt.cpp:124
static FWNETWORKIO_API Request::sptr New(const std::string &url)
Creates a new Request with given url.
Definition: Request.cpp:26
static FWNETWORKIO_API InstanceUIDContainer toSeriesInstanceUIDContainer(DicomSeriesContainer series)
Convert std::vector< ::fwMedData::DicomSeries > to series instance uid container. ...
virtual FWGUI_API void setMessage(const std::string &msg) override
Set the message.
IODICOMWEB_API void updating() override
Checks the configuration and pull the series.
Defines the generic message box for IHM. Use the Delegate design pattern.
FWNETWORKIO_API std::string getFile(Request::sptr request)
Retrieves data over network.
Definition: ClientQt.cpp:58
Defines an helper to modify an fwMedData::SeriesDB and create in parallel the message to announce thi...
#define SLM_WARN(message)
Definition: spyLog.hpp:261
virtual FWGUI_API void addButton(IMessageDialog::Buttons button) override
Add a button (OK, YES_NO, YES, NO, CANCEL)
FWNETWORKIO_API QByteArray get(Request::sptr request)
Retrieves data over network.
Definition: ClientQt.cpp:36
virtual FWGUI_API IMessageDialog::Buttons show() override
Show the message box and return the clicked button.
#define SLM_ASSERT(message, cond)
work like &#39;assert&#39; from &#39;cassert&#39;, with in addition a message logged by spylog (with FATAL loglevel) ...
Definition: spyLog.hpp:308
virtual IODICOMWEB_API void configuring() override
Gets the configuration.
::fwRuntime::ConfigurationElement::sptr m_configuration
Configuration element used to configure service internal state using a generic XML like structure TOD...
Definition: IService.hpp:670
virtual FWGUI_API void setIcon(IMessageDialog::Icons icon) override
Set the icon (CRITICAL, WARNING, INFO or QUESTION)
Implements exception for HTTP content not found errors.
Implements exception for an HTTP host not found errors.
ioDicomWeb contains services use to deal with PACS through HTTP.
FWMEDDATATOOLS_API void notify()
Send the signal of modification.
static FWSERVICES_API ServiceFactory::sptr getDefault()
Return the unique Instance, create it if required at first access.
virtual IODICOMWEB_API void starting() override
Registers the DICOM reader.
static FWSERVICES_API ServiceConfig::sptr getDefault()
Return the default global instance of ServiceConfig.
virtual IODICOMWEB_API ~SSeriesPuller() noexcept
Destructor.
IODICOMWEB_API SSeriesPuller() noexcept
Constructor.
virtual FWGUI_API void setTitle(const std::string &title) override
Set the title of the message box.
FWMEDDATATOOLS_API void merge(::fwMedData::SeriesDB::sptr seriesDBIn)
Merge seriesDBIn all series from seriesDBIn to the SeriesDB.
FWSERVICES_API ConfigType getConfigTree() const
Return the configuration, in an boost property tree.
Definition: IService.cpp:247