Browse Source

Merge pull request #569 from AndreasFetzer/refactor-upload-and-implement-file-upload-integration

XNAT API: Refactor upload and implement file upload
Marco Nolden 10 years ago
parent
commit
094294cbea
31 changed files with 1212 additions and 114 deletions
  1. 63 8
      Applications/ctkXnatTreeBrowser/ctkXnatTreeBrowserMainWindow.cpp
  2. 2 0
      Applications/ctkXnatTreeBrowser/ctkXnatTreeBrowserMainWindow.h
  3. 26 3
      Applications/ctkXnatTreeBrowser/ctkXnatTreeBrowserMainWindow.ui
  4. 1 1
      CMakeExternals/qRestAPI.cmake
  5. 2 2
      Libs/Widgets/Testing/Cpp/ctkSearchBoxTest1.cpp
  6. 2 0
      Libs/XNAT/Core/CMakeLists.txt
  7. 170 3
      Libs/XNAT/Core/Testing/ctkXnatSessionTest.cpp
  8. 4 0
      Libs/XNAT/Core/Testing/ctkXnatSessionTest.h
  9. 12 1
      Libs/XNAT/Core/ctkXnatAPI.cpp
  10. 12 0
      Libs/XNAT/Core/ctkXnatAssessorFolder.cpp
  11. 3 0
      Libs/XNAT/Core/ctkXnatAssessorFolder.h
  12. 56 5
      Libs/XNAT/Core/ctkXnatFile.cpp
  13. 8 1
      Libs/XNAT/Core/ctkXnatFile.h
  14. 121 29
      Libs/XNAT/Core/ctkXnatObject.cpp
  15. 23 5
      Libs/XNAT/Core/ctkXnatObject.h
  16. 13 0
      Libs/XNAT/Core/ctkXnatReconstructionFolder.cpp
  17. 3 0
      Libs/XNAT/Core/ctkXnatReconstructionFolder.h
  18. 60 1
      Libs/XNAT/Core/ctkXnatResource.cpp
  19. 13 1
      Libs/XNAT/Core/ctkXnatResource.h
  20. 81 0
      Libs/XNAT/Core/ctkXnatResourceCatalogXmlParser.cpp
  21. 73 0
      Libs/XNAT/Core/ctkXnatResourceCatalogXmlParser.h
  22. 120 0
      Libs/XNAT/Core/ctkXnatResourceFolder.cpp
  23. 60 0
      Libs/XNAT/Core/ctkXnatResourceFolder.h
  24. 12 0
      Libs/XNAT/Core/ctkXnatScanFolder.cpp
  25. 3 0
      Libs/XNAT/Core/ctkXnatScanFolder.h
  26. 115 39
      Libs/XNAT/Core/ctkXnatSession.cpp
  27. 45 2
      Libs/XNAT/Core/ctkXnatSession.h
  28. 89 10
      Libs/XNAT/Core/ctkXnatTreeModel.cpp
  29. 8 1
      Libs/XNAT/Core/ctkXnatTreeModel.h
  30. 1 0
      Libs/XNAT/Widgets/ctkXnatLoginDialog.cpp
  31. 11 2
      Libs/XNAT/Widgets/ctkXnatLoginDialog.ui

+ 63 - 8
Applications/ctkXnatTreeBrowser/ctkXnatTreeBrowserMainWindow.cpp

@@ -22,19 +22,27 @@
 #include "ctkXnatTreeBrowserMainWindow.h"
 #include "ui_ctkXnatTreeBrowserMainWindow.h"
 
-#include "ctkXnatLoginDialog.h"
-#include "ctkXnatTreeModel.h"
-#include "ctkXnatSession.h"
+#include "ctkXnatAssessor.h"
+#include "ctkXnatAssessorFolder.h"
 #include "ctkXnatDataModel.h"
-#include "ctkXnatProject.h"
+#include "ctkXnatException.h"
+#include "ctkXnatExperiment.h"
 #include "ctkXnatFile.h"
+#include "ctkXnatLoginDialog.h"
+#include "ctkXnatProject.h"
+#include "ctkXnatReconstruction.h"
+#include "ctkXnatReconstructionFolder.h"
 #include "ctkXnatResource.h"
+#include "ctkXnatResourceFolder.h"
 #include "ctkXnatScan.h"
 #include "ctkXnatScanFolder.h"
-#include "ctkXnatAssessor.h"
-#include "ctkXnatAssessorFolder.h"
-#include "ctkXnatReconstruction.h"
-#include "ctkXnatReconstructionFolder.h"
+#include "ctkXnatSession.h"
+#include "ctkXnatSubject.h"
+#include "ctkXnatTreeModel.h"
+
+#include <QMessageBox>
+#include <QFileDialog>
+#include <QFileInfo>
 
 ctkXnatTreeBrowserMainWindow::ctkXnatTreeBrowserMainWindow(QWidget *parent) :
   QMainWindow(parent),
@@ -50,6 +58,8 @@ ctkXnatTreeBrowserMainWindow::ctkXnatTreeBrowserMainWindow(QWidget *parent) :
   this->connect(ui->loginButton, SIGNAL(clicked()), SLOT(loginButtonPushed()));
   this->connect(ui->treeView, SIGNAL(clicked(const QModelIndex&)), SLOT(itemSelected(const QModelIndex&)));
   this->connect(ui->downloadButton, SIGNAL(clicked()), SLOT(downloadButtonClicked()));
+  this->connect(ui->addResourceButton, SIGNAL(clicked()), SLOT(addResourceClicked()));
+  this->connect(ui->uploadFileButton, SIGNAL(clicked()), SLOT(uploadFileClicked()));
 }
 
 ctkXnatTreeBrowserMainWindow::~ctkXnatTreeBrowserMainWindow()
@@ -110,6 +120,16 @@ void ctkXnatTreeBrowserMainWindow::itemSelected(const QModelIndex &index)
   downloadable |= dynamic_cast<ctkXnatReconstructionFolder*>(xnatObject)!=NULL;
   ui->downloadButton->setEnabled(downloadable);
   ui->downloadLabel->setVisible(!downloadable);
+  bool canHaveResource = false;
+  canHaveResource |= dynamic_cast<ctkXnatProject*>(xnatObject) != NULL;
+  canHaveResource |=  dynamic_cast<ctkXnatSubject*>(xnatObject) != NULL;
+  canHaveResource |=  dynamic_cast<ctkXnatExperiment*>(xnatObject) != NULL;
+  ui->addResourceButton->setEnabled(canHaveResource);
+  bool uploadFilePossible = false;
+  uploadFilePossible |= dynamic_cast<ctkXnatResource*>(xnatObject) != NULL;
+  uploadFilePossible |=  dynamic_cast<ctkXnatScan*>(xnatObject) != NULL;
+  uploadFilePossible |=  dynamic_cast<ctkXnatAssessor*>(xnatObject) != NULL;
+  ui->uploadFileButton->setEnabled(uploadFilePossible);
 }
 
 void ctkXnatTreeBrowserMainWindow::downloadButtonClicked()
@@ -122,3 +142,38 @@ void ctkXnatTreeBrowserMainWindow::downloadButtonClicked()
     m_TreeModel->downloadFile(index, fileName);
   }
 }
+
+void ctkXnatTreeBrowserMainWindow::addResourceClicked()
+{
+  const QModelIndex index = ui->treeView->selectionModel()->currentIndex();
+  ctkXnatObject* parentObject = m_TreeModel->xnatObject(index);
+  parentObject->addResourceFolder("data");
+  m_TreeModel->refresh(index);
+}
+
+void ctkXnatTreeBrowserMainWindow::uploadFileClicked()
+{
+  QString filename = QFileDialog::getOpenFileName(this, tr("Open File"), QDir::homePath());
+  const QModelIndex index = ui->treeView->selectionModel()->currentIndex();
+  ctkXnatResource* resource = dynamic_cast<ctkXnatResource*>(m_TreeModel->xnatObject(index));
+  if (resource)
+  {
+    ctkXnatFile* file = new ctkXnatFile();
+    file->setLocalFilePath(filename);
+    QFileInfo fileInfo (filename);
+    file->setName(fileInfo.fileName());
+    resource->add(file);
+    try
+    {
+      file->save();
+    }
+    catch (ctkXnatException &e)
+    {
+      QMessageBox msgbox;
+      msgbox.setText(e.what());
+      msgbox.setIcon(QMessageBox::Critical);
+      msgbox.exec();
+    }
+    m_TreeModel->addChildNode(index, file);
+  }
+}

+ 2 - 0
Applications/ctkXnatTreeBrowser/ctkXnatTreeBrowserMainWindow.h

@@ -46,6 +46,8 @@ private Q_SLOTS:
   void loginButtonPushed();
   void itemSelected(const QModelIndex&);
   void downloadButtonClicked();
+  void addResourceClicked();
+  void uploadFileClicked();
 
 private:
   Ui::ctkXnatTreeBrowserMainWindow* ui;

+ 26 - 3
Applications/ctkXnatTreeBrowser/ctkXnatTreeBrowserMainWindow.ui

@@ -6,7 +6,7 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>409</width>
+    <width>553</width>
     <height>325</height>
    </rect>
   </property>
@@ -25,6 +25,29 @@
        </widget>
       </item>
       <item>
+       <widget class="QPushButton" name="addResourceButton">
+        <property name="enabled">
+         <bool>false</bool>
+        </property>
+        <property name="toolTip">
+         <string>Creates a new resource folder</string>
+        </property>
+        <property name="text">
+         <string>Add Resource</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QPushButton" name="uploadFileButton">
+        <property name="enabled">
+         <bool>false</bool>
+        </property>
+        <property name="text">
+         <string>Upload File</string>
+        </property>
+       </widget>
+      </item>
+      <item>
        <widget class="QPushButton" name="downloadButton">
         <property name="enabled">
          <bool>false</bool>
@@ -85,8 +108,8 @@ font: 75 10pt &quot;Lucida Grande&quot;;</string>
     <rect>
      <x>0</x>
      <y>0</y>
-     <width>409</width>
-     <height>21</height>
+     <width>553</width>
+     <height>22</height>
     </rect>
    </property>
    <widget class="QMenu" name="menu_File">

+ 1 - 1
CMakeExternals/qRestAPI.cmake

@@ -24,7 +24,7 @@ endif()
 
 if(NOT DEFINED ${proj}_DIR)
 
-  set(revision_tag "5f3a03b15d")
+  set(revision_tag "4293694a89")
   if(${proj}_REVISION_TAG)
     set(revision_tag ${${proj}_REVISION_TAG})
   endif()

+ 2 - 2
Libs/Widgets/Testing/Cpp/ctkSearchBoxTest1.cpp

@@ -38,8 +38,8 @@ int ctkSearchBoxTest1(int argc, char* argv[])
   QApplication app(argc, argv);
 
   QPalette p;
-  p.setColor(QPalette::ColorRole::Window, Qt::gray);
-  p.setColor(QPalette::ColorRole::Base, Qt::gray);
+  p.setColor(QPalette::Window, Qt::gray);
+  p.setColor(QPalette::Base, Qt::gray);
 
   ctkSearchBox search;
   search.setShowSearchIcon(true);

+ 2 - 0
Libs/XNAT/Core/CMakeLists.txt

@@ -21,10 +21,12 @@ set(KIT_SRCS
   ctkXnatReconstruction.cpp
   ctkXnatReconstructionFolder.cpp
   ctkXnatResource.cpp
+  ctkXnatResourceCatalogXmlParser.cpp
   ctkXnatScan.cpp
   ctkXnatScanFolder.cpp
   ctkXnatAssessor.cpp
   ctkXnatAssessorFolder.cpp
+  ctkXnatResourceFolder.cpp
   ctkXnatSession.cpp
   ctkXnatSettings.cpp
   ctkXnatSubject.cpp

+ 170 - 3
Libs/XNAT/Core/Testing/ctkXnatSessionTest.cpp

@@ -21,6 +21,7 @@
 #include "ctkXnatSessionTest.h"
 
 #include <QCoreApplication>
+#include <QCryptographicHash>
 #include <QDebug>
 #include <QDir>
 #include <QSignalSpy>
@@ -32,11 +33,14 @@
 #include <QUuid>
 
 #include <ctkXnatDataModel.h>
+#include <ctkXnatException.h>
+#include <ctkXnatFile.h>
 #include <ctkXnatLoginProfile.h>
-#include <ctkXnatSession.h>
 #include <ctkXnatProject.h>
+#include <ctkXnatResource.h>
+#include <ctkXnatResourceFolder.h>
+#include <ctkXnatSession.h>
 #include <ctkXnatSubject.h>
-#include <ctkXnatException.h>
 
 class ctkXnatSessionTestCasePrivate
 {
@@ -74,6 +78,7 @@ void ctkXnatSessionTestCase::initTestCase()
   d->LoginProfile.setPassword("ctk-xnat2015");
 }
 
+// --------------------------------------------------------------------------
 void ctkXnatSessionTestCase::init()
 {
   Q_D(ctkXnatSessionTestCase);
@@ -83,10 +88,12 @@ void ctkXnatSessionTestCase::init()
   d->Session->open();
 }
 
+// --------------------------------------------------------------------------
 void ctkXnatSessionTestCase::cleanupTestCase()
 {
 }
 
+// --------------------------------------------------------------------------
 void ctkXnatSessionTestCase::cleanup()
 {
   Q_D(ctkXnatSessionTestCase);
@@ -95,6 +102,7 @@ void ctkXnatSessionTestCase::cleanup()
   d->Session = NULL;
 }
 
+// --------------------------------------------------------------------------
 void ctkXnatSessionTestCase::testProjectList()
 {
   Q_D(ctkXnatSessionTestCase);
@@ -107,6 +115,7 @@ void ctkXnatSessionTestCase::testProjectList()
   QVERIFY(projects.size() > 0);
 }
 
+// --------------------------------------------------------------------------
 void ctkXnatSessionTestCase::testResourceUri()
 {
   Q_D(ctkXnatSessionTestCase);
@@ -116,6 +125,7 @@ void ctkXnatSessionTestCase::testResourceUri()
   QVERIFY(dataModel->resourceUri().isEmpty());
 }
 
+// --------------------------------------------------------------------------
 void ctkXnatSessionTestCase::testParentChild()
 {
   Q_D(ctkXnatSessionTestCase);
@@ -153,6 +163,7 @@ void ctkXnatSessionTestCase::testParentChild()
   delete project;
 }
 
+// --------------------------------------------------------------------------
 void ctkXnatSessionTestCase::testSession()
 {
   Q_D(ctkXnatSessionTestCase);
@@ -188,6 +199,7 @@ void ctkXnatSessionTestCase::testSession()
   {}
 }
 
+// --------------------------------------------------------------------------
 void ctkXnatSessionTestCase::testAuthenticationError()
 {
   ctkXnatLoginProfile loginProfile;
@@ -206,6 +218,7 @@ void ctkXnatSessionTestCase::testAuthenticationError()
   {}
 }
 
+// --------------------------------------------------------------------------
 void ctkXnatSessionTestCase::testCreateProject()
 {
   Q_D(ctkXnatSessionTestCase);
@@ -223,7 +236,7 @@ void ctkXnatSessionTestCase::testCreateProject()
   bool exists = d->Session->exists(project);
   QVERIFY(!exists);
 
-  d->Session->save(project);
+  project->save();
 
   exists = d->Session->exists(project);
   QVERIFY(exists);
@@ -234,6 +247,7 @@ void ctkXnatSessionTestCase::testCreateProject()
   QVERIFY(!exists);
 }
 
+// --------------------------------------------------------------------------
 void ctkXnatSessionTestCase::testCreateSubject()
 {
   Q_D(ctkXnatSessionTestCase);
@@ -274,6 +288,159 @@ void ctkXnatSessionTestCase::testCreateSubject()
 }
 
 // --------------------------------------------------------------------------
+void ctkXnatSessionTestCase::testAddResourceFolder()
+{
+  Q_D(ctkXnatSessionTestCase);
+
+  ctkXnatDataModel* dataModel = d->Session->dataModel();
+
+  QString projectId = QString("CTK_") + QUuid::createUuid().toString().mid(1, 8);
+  d->Project = projectId;
+
+  ctkXnatProject* project = new ctkXnatProject(dataModel);
+  project->setId(projectId);
+  project->setName(projectId);
+  project->setDescription("CTK_test_project");
+
+  QVERIFY(!project->exists());
+
+  project->save();
+
+  QVERIFY(project->exists());
+
+  ctkXnatResource* resource = project->addResourceFolder("TestResource", "testFormat", "testContent", "testTag1,testTag2");
+  QVERIFY(resource->exists());
+  QVERIFY(resource->name() == "TestResource");
+  QVERIFY(resource->format() == "testFormat");
+  QVERIFY(resource->content() == "testContent");
+  QVERIFY(resource->tags() == "testTag1,testTag2");
+
+  ctkXnatResourceFolder* folder = dynamic_cast<ctkXnatResourceFolder*>(resource->parent());
+  QVERIFY(folder->exists());
+  QVERIFY(folder != 0);
+  QVERIFY(folder->name() == "Resources");
+
+  project->erase();
+  QVERIFY(!project->exists());
+  QVERIFY(!folder->exists());
+  QVERIFY(!resource->exists());
+}
+
+// --------------------------------------------------------------------------
+void ctkXnatSessionTestCase::testUploadAndDownloadFile()
+{
+  Q_D(ctkXnatSessionTestCase);
+
+  ctkXnatDataModel* dataModel = d->Session->dataModel();
+
+  QString projectId = QString("CTK_") + QUuid::createUuid().toString().mid(1, 8);
+  d->Project = projectId;
+
+  ctkXnatProject* project = new ctkXnatProject(dataModel);
+  project->setId(projectId);
+  project->setName(projectId);
+  project->setDescription("CTK_test_project");
+
+  QVERIFY(!project->exists());
+
+  project->save();
+
+  QVERIFY(project->exists());
+
+  ctkXnatResource* resource = project->addResourceFolder("TestResourceContainingData");
+  QVERIFY(resource->exists());
+  QVERIFY(resource->name() == "TestResourceContainingData");
+  QVERIFY(resource->format() == "");
+  QVERIFY(resource->content() == "");
+  QVERIFY(resource->tags() == "");
+
+  QString tempDirPath = QDir::tempPath() + QUuid::createUuid().toString().mid(1, 8);
+  QString uploadFileName = tempDirPath + "/ctk_xnat_upload_" + QUuid::createUuid().toString().mid(1, 8) + ".txt";
+  QString downloadFileName = tempDirPath + "/ctk_xnat_download_" + QUuid::createUuid().toString().mid(1, 8) + ".txt";
+
+  QDir tempDir;
+  if (tempDir.mkdir(tempDirPath))
+  {
+    QFile uploadedFile(uploadFileName);
+
+    if (uploadedFile.open(QFile::ReadWrite))
+    {
+      QTextStream stream( &uploadedFile );
+      stream << "Hi, I am a CTK test file! ;-)" << endl;
+
+      QFileInfo fileInfo;
+      fileInfo.setFile(uploadFileName);
+      // Create xnatFile object
+      ctkXnatFile* xnatFile = new ctkXnatFile(resource);
+      xnatFile->setLocalFilePath(fileInfo.filePath());
+      xnatFile->setName(fileInfo.fileName());
+      xnatFile->setFileFormat("some format");
+      xnatFile->setFileContent("some content");
+      xnatFile->setFileTags("some, tags");
+      resource->add(xnatFile);
+
+      // Actual file upload
+      xnatFile->save();
+
+      QVERIFY(xnatFile->exists());
+
+      xnatFile->download(downloadFileName);
+
+      QFile downloadedFile(downloadFileName);
+
+      QVERIFY(downloadedFile.exists());
+
+      uploadedFile.close();
+      if (downloadedFile.open(QFile::ReadOnly) && uploadedFile.open(QFile::ReadOnly))
+      {
+        QCryptographicHash hashUploaded(QCryptographicHash::Md5);
+        QCryptographicHash hashDownloaded(QCryptographicHash::Md5);
+
+#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
+        hashUploaded.addData(&uploadedFile);
+        hashDownloaded.addData(&downloadedFile);
+#else
+        hashUploaded.addData(uploadedFile.readAll());
+        hashDownloaded.addData(downloadedFile.readAll());
+#endif
+
+        QString md5ChecksumUploaded(hashUploaded.result().toHex());
+        QString md5ChecksumDownloaded(hashDownloaded.result().toHex());
+
+        QVERIFY (md5ChecksumDownloaded == md5ChecksumUploaded);
+
+        // Remove the data from XNAT
+        project->erase();
+        QVERIFY(!project->exists());
+        QVERIFY(!resource->exists());
+        QVERIFY(!xnatFile->exists());
+
+        // Remove the local data
+        uploadedFile.close();
+        downloadedFile.close();
+        uploadedFile.remove();
+        downloadedFile.remove();
+        tempDir.cdUp();
+        tempDir.rmdir(tempDirPath);
+      }
+      else
+      {
+        qWarning()<<"Could not open files for validation! Could not finish test!";
+      }
+    }
+    else
+    {
+      qWarning()<<"Could not create temporary file for upload! Could not finish test!";
+      return;
+    }
+  }
+  else
+  {
+    qWarning()<<"Could not create temporary directory! Could not finish test!";
+  }
+}
+
+// --------------------------------------------------------------------------
 int ctkXnatSessionTest(int argc, char* argv[])
 {
   QCoreApplication app(argc, argv);

+ 4 - 0
Libs/XNAT/Core/Testing/ctkXnatSessionTest.h

@@ -60,6 +60,10 @@ private slots:
 
   void testCreateSubject();
 
+  void testAddResourceFolder();
+
+  void testUploadAndDownloadFile();
+
 private:
   QScopedPointer<ctkXnatSessionTestCasePrivate> d_ptr;
 

+ 12 - 1
Libs/XNAT/Core/ctkXnatAPI.cpp

@@ -21,7 +21,11 @@
 
 // ctkXnatAPI includes
 #include "ctkXnatAPI_p.h"
+
+#include "ctkXnatResourceCatalogXmlParser.h"
+
 #include "qRestResult.h"
+
 #include <QNetworkReply>
 #include <QRegExp>
 #include <QUrl>
@@ -108,9 +112,16 @@ void ctkXnatAPI::parseResponse(qRestResult* restResult, const QByteArray& respon
 }
 
 // --------------------------------------------------------------------------
-QList<QVariantMap> ctkXnatAPI::parseXmlResponse(qRestResult* /*restResult*/, const QByteArray& /*response*/)
+QList<QVariantMap> ctkXnatAPI::parseXmlResponse(qRestResult* /*restResult*/, const QByteArray& response)
 {
   QList<QVariantMap> result;
+  // In this case a resource catalog xml was requested
+  if (response.contains("<cat:Catalog"))
+  {
+    ctkXnatResourceCatalogXmlParser parser;
+    parser.setData(response);
+    parser.parseXml(result);
+  }
   return result;
 }
 

+ 12 - 0
Libs/XNAT/Core/ctkXnatAssessorFolder.cpp

@@ -66,6 +66,18 @@ QString ctkXnatAssessorFolder::resourceUri() const
 }
 
 //----------------------------------------------------------------------------
+QString ctkXnatAssessorFolder::name() const
+{
+  return this->label();
+}
+
+//----------------------------------------------------------------------------
+QString ctkXnatAssessorFolder::label() const
+{
+  return this->property(LABEL);
+}
+
+//----------------------------------------------------------------------------
 void ctkXnatAssessorFolder::reset()
 {
   ctkXnatObject::reset();

+ 3 - 0
Libs/XNAT/Core/ctkXnatAssessorFolder.h

@@ -42,6 +42,9 @@ public:
 
   virtual QString resourceUri() const;
 
+  virtual QString name() const;
+  virtual QString label()const;
+
   void reset();
 
 private:

+ 56 - 5
Libs/XNAT/Core/ctkXnatFile.cpp

@@ -41,10 +41,9 @@ public:
 
   void reset()
   {
-//    uri.clear();
   }
 
-//  QString uri;
+  QString localFilePath;
 };
 
 
@@ -77,6 +76,7 @@ void ctkXnatFile::setFileFormat(const QString &fileFormat)
   this->setProperty(FILE_FORMAT, fileFormat);
 }
 
+//----------------------------------------------------------------------------
 QString ctkXnatFile::fileFormat() const
 {
   return this->property(FILE_FORMAT);
@@ -88,6 +88,7 @@ void ctkXnatFile::setFileContent(const QString &fileContent)
   this->setProperty(FILE_CONTENT, fileContent);
 }
 
+//----------------------------------------------------------------------------
 QString ctkXnatFile::fileContent() const
 {
   return this->property(FILE_CONTENT);
@@ -99,20 +100,30 @@ void ctkXnatFile::setFileTags(const QString &fileTags)
   this->setProperty(FILE_TAGS, fileTags);
 }
 
+//----------------------------------------------------------------------------
 QString ctkXnatFile::fileTags() const
 {
   return this->property(FILE_TAGS);
 }
 
 //----------------------------------------------------------------------------
-QString ctkXnatFile::resourceUri() const
+void ctkXnatFile::setLocalFilePath(const QString &filePath)
 {
-  return QString("%1/files/%2").arg(parent()->resourceUri(), this->name());
+  Q_D(ctkXnatFile);
+  d->localFilePath = filePath;
+}
+
+//----------------------------------------------------------------------------
+QString ctkXnatFile::localFilePath() const
+{
+  Q_D(const ctkXnatFile);
+  return d->localFilePath;
 }
 
 //----------------------------------------------------------------------------
-void ctkXnatFile::upload(const QString& /*filename*/)
+QString ctkXnatFile::resourceUri() const
 {
+  return QString("%1/files/%2").arg(parent()->resourceUri(), this->name());
 }
 
 //----------------------------------------------------------------------------
@@ -124,6 +135,7 @@ void ctkXnatFile::reset()
 //----------------------------------------------------------------------------
 void ctkXnatFile::fetchImpl()
 {
+  // Does not make sense to fetch a file
 }
 
 //----------------------------------------------------------------------------
@@ -132,3 +144,42 @@ void ctkXnatFile::downloadImpl(const QString& filename)
   QString query = this->resourceUri();
   this->session()->download(filename, query);
 }
+
+//----------------------------------------------------------------------------
+void ctkXnatFile::saveImpl(bool overwrite)
+{
+  Q_D(ctkXnatFile);
+
+  ctkXnatSession::UrlParameters urlParams;
+  urlParams["xsi:type"] = this->schemaType();
+  // Flag needed for file upload
+  urlParams["inbody"] = "true";
+
+  // Creating the update query
+  const QMap<QString, QString>& properties = this->properties();
+  QMapIterator<QString, QString> itProperties(properties);
+  while (itProperties.hasNext())
+  {
+    itProperties.next();
+
+    // Do not append these file specific properties since they require a slightly
+    // different key for uploading a file (e.g. instead of "file_format" only "format")
+    if (itProperties.key() == FILE_TAGS || itProperties.key() == FILE_FORMAT ||
+        itProperties.key() == FILE_CONTENT)
+      continue;
+
+    urlParams[itProperties.key()] = itProperties.value();
+  }
+
+  if (!this->fileFormat().isNull())
+    urlParams["format"] = this->fileFormat();
+  if (!this->fileContent().isNull())
+    urlParams["content"] = this->fileContent();
+  if (!this->fileContent().isNull())
+    urlParams["tags"] = this->fileTags();
+
+  if (this->exists() && overwrite)
+    urlParams["overwrite"] = "true";
+
+  this->session()->upload(this, urlParams);
+}

+ 8 - 1
Libs/XNAT/Core/ctkXnatFile.h

@@ -56,7 +56,8 @@ public:
   void setFileContent(const QString& fileContent);
   QString fileContent() const;
 
-  void upload(const QString& filename);
+  void setLocalFilePath(const QString& filepath);
+  QString localFilePath() const;
 
   void reset();
 
@@ -71,6 +72,12 @@ private:
 
   virtual void downloadImpl(const QString&);
 
+  /**
+    * @brief Uploads the file to the server
+    * Before calling save() the localFilePath has to be set
+    */
+  virtual void saveImpl(bool overwrite);
+
   Q_DECLARE_PRIVATE(ctkXnatFile)
 };
 

+ 121 - 29
Libs/XNAT/Core/ctkXnatObject.cpp

@@ -24,6 +24,9 @@
 
 #include "ctkXnatDataModel.h"
 #include "ctkXnatDefaultSchemaTypes.h"
+#include "ctkXnatException.h"
+#include "ctkXnatResource.h"
+#include "ctkXnatResourceFolder.h"
 #include "ctkXnatSession.h"
 
 #include <QDateTime>
@@ -112,7 +115,7 @@ QString ctkXnatObject::childDataType() const
   return "Resources";
 }
 
-QDateTime ctkXnatObject::lastModifiedTime()
+QDateTime ctkXnatObject::lastModifiedTimeOnServer()
 {
   Q_D(ctkXnatObject);
   QUuid queryId = this->session()->httpHead(this->resourceUri());
@@ -144,9 +147,6 @@ QDateTime ctkXnatObject::lastModifiedTime()
         break;
     }
   }
-
-  if (lastModifiedTime.isValid() && d->lastModifiedTime < lastModifiedTime)
-    this->setLastModifiedTime(lastModifiedTime);
   return lastModifiedTime;
 }
 
@@ -175,7 +175,10 @@ QString ctkXnatObject::property(const QString& name) const
 void ctkXnatObject::setProperty(const QString& name, const QVariant& value)
 {
   Q_D(ctkXnatObject);
-  d->properties.insert(name, value.toString());
+  if (d->properties[name] != value)
+  {
+    d->properties.insert(name, value.toString());
+  }
 }
 
 //----------------------------------------------------------------------------
@@ -224,13 +227,23 @@ void ctkXnatObject::add(ctkXnatObject* child)
   {
     child->d_func()->parent = this;
   }
-  if (!d->children.contains(child))
+
+  bool childExists (false);
+
+  QList<ctkXnatObject*>::iterator iter;
+  for (iter = d->children.begin(); iter != d->children.end(); ++iter)
   {
-    d->children.push_back(child);
+    if (((*iter)->id().length() != 0 && (*iter)->id() == child->id()) ||
+        ((*iter)->id().length() == 0 && (*iter)->name() == child->name()))
+    {
+      *iter = child;
+      childExists = true;
+    }
   }
-  else
+
+  if (!childExists)
   {
-    qWarning() << "ctkXnatObject::add(): Child already exists";
+    d->children.push_back(child);
   }
 }
 
@@ -273,10 +286,10 @@ void ctkXnatObject::setSchemaType(const QString& schemaType)
 }
 
 //----------------------------------------------------------------------------
-void ctkXnatObject::fetch()
+void ctkXnatObject::fetch(bool forceFetch)
 {
   Q_D(ctkXnatObject);
-  if (!d->fetched)
+  if (!d->fetched || forceFetch)
   {
     this->fetchImpl();
     d->fetched = true;
@@ -302,43 +315,122 @@ void ctkXnatObject::download(const QString& filename)
 }
 
 //----------------------------------------------------------------------------
-void ctkXnatObject::upload(const QString& /*zipFilename*/)
+void ctkXnatObject::save(bool overwrite)
 {
+  Q_D(ctkXnatObject);
+  this->saveImpl(overwrite);
 }
 
 //----------------------------------------------------------------------------
-bool ctkXnatObject::exists() const
+ctkXnatResource* ctkXnatObject::addResourceFolder(QString foldername, QString format,
+                                   QString content, QString tags)
 {
-  return this->session()->exists(this);
+  if (foldername.size() == 0)
+  {
+    throw ctkXnatException("Error creating resource! Foldername must not be empty!");
+  }
+
+  ctkXnatResourceFolder* resFolder = 0;
+  QList<ctkXnatObject*> children = this->children();
+  for (unsigned int i = 0; i < children.size(); ++i)
+  {
+    resFolder = dynamic_cast<ctkXnatResourceFolder*>(children.at(i));
+    if (resFolder)
+    {
+      break;
+    }
+  }
+
+  if (!resFolder)
+  {
+    resFolder = new ctkXnatResourceFolder();
+    this->add(resFolder);
+  }
+
+  ctkXnatResource* resource = new ctkXnatResource();
+  resource->setName(foldername);
+  if (format.size() != 0)
+    resource->setFormat(format);
+  if (content.size() != 0)
+    resource->setContent(content);
+  if (tags.size() != 0)
+    resource->setTags(tags);
+
+  resFolder->add(resource);
+
+  if (!resource->exists())
+    resource->save();
+  else
+    qDebug()<<"Not adding resource folder. Folder already exists!";
+
+  return resource;
 }
 
 //----------------------------------------------------------------------------
-void ctkXnatObject::save()
+bool ctkXnatObject::exists() const
 {
-  this->session()->save(this);
+  return this->session()->exists(this);
 }
 
 //----------------------------------------------------------------------------
-void ctkXnatObject::fetchResources(const QString& path)
+void ctkXnatObject::saveImpl(bool /*overwrite*/)
 {
-  QString query = this->resourceUri() + path;
-  ctkXnatSession* const session = this->session();
-  QUuid queryId = session->httpGet(query);
-
-  QList<ctkXnatObject*> resources = session->httpResults(queryId,
-                                                           ctkXnatDefaultSchemaTypes::XSI_RESOURCE);
+  Q_D(ctkXnatObject);
+  ctkXnatSession::UrlParameters urlParams;
+  urlParams["xsi:type"] = this->schemaType();
 
-  foreach (ctkXnatObject* resource, resources)
+  // Just do this if there is already a valid last-modification-time,
+  // otherwise the object is not yet on the server!
+  QDateTime remoteModTime;
+  if (d->lastModifiedTime.isValid())
   {
-    QString label = resource->name();
-    if (label.isEmpty())
+    // TODO Overwrite this for e.g. project and subject which already support modification time!
+    remoteModTime = this->lastModifiedTimeOnServer();
+    // If the object has been modified on the server, perform an update
+    if (d->lastModifiedTime < remoteModTime)
     {
-      label = "NO NAME";
+      qWarning()<<"Uploaded object maybe overwritten on server!";
+      // TODO update from server, since modification time is not really supported
+      // by xnat right now this is not of high priority
+      // something like this->updateImpl + setLastModifiedTime()
     }
+  }
+
+  const QMap<QString, QString>& properties = this->properties();
+  QMapIterator<QString, QString> itProperties(properties);
+  while (itProperties.hasNext())
+  {
+    itProperties.next();
+    if (itProperties.key() == "ID")
+      continue;
 
-    resource->setName(label);
-    this->add(resource);
+    urlParams[itProperties.key()] = itProperties.value();
   }
+
+  // Execute the update
+  QUuid queryID = this->session()->httpPut(this->resourceUri(), urlParams);
+  const QList<QVariantMap> results = this->session()->httpSync(queryID);
+
+  // If this xnat object did not exist before on the server set the ID returned by Xnat
+  if (results.size() == 1 && results[0].size() == 1)
+  {
+    QVariant id = results[0][ID];
+    if (!id.isNull())
+    {
+      this->setId(id.toString());
+    }
+  }
+
+  // Finally update the modification time on the server
+  remoteModTime = this->lastModifiedTimeOnServer();
+  d->lastModifiedTime = remoteModTime;
+}
+
+//----------------------------------------------------------------------------
+void ctkXnatObject::fetchResources(const QString& path)
+{
+  ctkXnatResourceFolder* resFolder = new ctkXnatResourceFolder();
+  this->add(resFolder);
 }
 
 //----------------------------------------------------------------------------

+ 23 - 5
Libs/XNAT/Core/ctkXnatObject.h

@@ -31,6 +31,7 @@
 #include <QString>
 #include <QMetaType>
 
+class ctkXnatResource;
 class ctkXnatSession;
 class ctkXnatObjectPrivate;
 
@@ -78,7 +79,7 @@ public:
   void setProperty(const QString& name, const QVariant& value);
 
   /// Gets the last modification time from the server
-  QDateTime lastModifiedTime();
+  virtual QDateTime lastModifiedTimeOnServer();
 
   /// Sets the last modfication time on the server
   void setLastModifiedTime(const QDateTime& lastModifiedTime);
@@ -100,7 +101,7 @@ public:
   /// Adds an object to the children of the current one.
   void add(ctkXnatObject* child);
 
-  /// Removes the object from the children of the current object and removes it from the XNAT server.
+  /// Removes the object from the children of the current object.
   void remove(ctkXnatObject* child);
 
   /// Tells if the children and the properties of the objects have been fetched.
@@ -116,19 +117,30 @@ public:
   virtual void reset();
 
   /// Fetches the children and the properties of the object.
-  void fetch();
+  void fetch(bool forceFetch = false);
 
   /// Checks if the object exists on the XNAT server.
   bool exists() const;
 
   /// Creates the object on the XNAT server and sets the new ID.
-  void save();
+  /// @param overwrite, if true and the object already exists on the server
+  ///                   it will be overwritten by the changes
+  void save(bool overwrite = true);
 
   /// Deletes the object on the XNAT server and removes it from its parent.
   void erase();
 
   void download(const QString&);
-  virtual void upload(const QString&);
+
+  /// Creates a new resource folder with the given foldername, format, content and tags
+  /// for this ctkXnatObject on the server.
+  /// @param foldername the name of the resource folder on the server (mandatory)
+  /// @param format the text of the format field of a resource (optional)
+  /// @param content the text of the content field of a resource (optional)
+  /// @param tags the content of the tags field of a resource (optional)
+  /// @returns ctkXnatResource the created resource
+  virtual ctkXnatResource* addResourceFolder(QString foldername,
+                           QString format = "", QString content = "", QString tags = "");
 
   //QObject* asyncObject() const;
 
@@ -181,6 +193,12 @@ private:
   /// The implementation of the download mechanism, called by the download(const QString&) function.
   virtual void downloadImpl(const QString&) = 0;
 
+  /// The implementation of the upload mechanism, called by the save() function.
+  /// Subclasses of ctkXnatObject can overwrite this function if needed
+  /// @param overwrite, if true and the object already exists on the server
+  ///                   it will be overwritten by the changes
+  virtual void saveImpl(bool overwrite = true);
+
   Q_DECLARE_PRIVATE(ctkXnatObject)
 };
 

+ 13 - 0
Libs/XNAT/Core/ctkXnatReconstructionFolder.cpp

@@ -53,6 +53,7 @@ ctkXnatReconstructionFolder::ctkXnatReconstructionFolder(ctkXnatObject* parent)
   : ctkXnatObject(*new ctkXnatReconstructionFolderPrivate(), parent, QString::null)
 {
   this->setProperty(ID, "reconstructions");
+  this->setProperty(LABEL, "Reconstructions");
 }
 
 //----------------------------------------------------------------------------
@@ -67,6 +68,18 @@ QString ctkXnatReconstructionFolder::resourceUri() const
 }
 
 //----------------------------------------------------------------------------
+QString ctkXnatReconstructionFolder::name() const
+{
+  return this->label();
+}
+
+//----------------------------------------------------------------------------
+QString ctkXnatReconstructionFolder::label() const
+{
+  return this->property(LABEL);
+}
+
+//----------------------------------------------------------------------------
 void ctkXnatReconstructionFolder::reset()
 {
   ctkXnatObject::reset();

+ 3 - 0
Libs/XNAT/Core/ctkXnatReconstructionFolder.h

@@ -44,6 +44,9 @@ public:
 
   virtual QString resourceUri() const;
 
+  virtual QString name() const;
+  virtual QString label()const;
+
   void reset();
 
 private:

+ 60 - 1
Libs/XNAT/Core/ctkXnatResource.cpp

@@ -24,7 +24,11 @@
 #include "ctkXnatObjectPrivate.h"
 #include "ctkXnatSession.h"
 
+const QString ctkXnatResource::TAGS = "tags";
+const QString ctkXnatResource::FORMAT = "format";
+const QString ctkXnatResource::CONTENT = "content";
 const QString ctkXnatResource::ID = "xnat_abstractresource_id";
+
 //----------------------------------------------------------------------------
 class ctkXnatResourcePrivate : public ctkXnatObjectPrivate
 {
@@ -51,7 +55,7 @@ ctkXnatResource::~ctkXnatResource()
 //----------------------------------------------------------------------------
 QString ctkXnatResource::resourceUri() const
 {
-  return QString("%1/resources/%2").arg(parent()->resourceUri(), this->id());
+  return QString("%1/%2").arg(parent()->resourceUri(), this->name());
 }
 
 //----------------------------------------------------------------------------
@@ -91,6 +95,41 @@ void ctkXnatResource::setLabel(const QString &label)
 }
 
 //----------------------------------------------------------------------------
+void ctkXnatResource::setFormat(const QString &fileFormat)
+{
+  this->setProperty(FORMAT, fileFormat);
+}
+
+//----------------------------------------------------------------------------
+QString ctkXnatResource::format() const
+{
+  return this->property(FORMAT);
+}
+
+//----------------------------------------------------------------------------
+void ctkXnatResource::setContent(const QString &fileContent)
+{
+  this->setProperty(CONTENT, fileContent);
+}
+
+//----------------------------------------------------------------------------
+QString ctkXnatResource::content() const
+{
+  return this->property(CONTENT);
+}
+
+//----------------------------------------------------------------------------
+void ctkXnatResource::setTags(const QString &fileTags)
+{
+  this->setProperty(TAGS, fileTags);
+}
+
+//----------------------------------------------------------------------------
+QString ctkXnatResource::tags() const
+{
+  return this->property(TAGS);
+}
+//----------------------------------------------------------------------------
 void ctkXnatResource::reset()
 {
   ctkXnatObject::reset();
@@ -126,3 +165,23 @@ void ctkXnatResource::downloadImpl(const QString& filename)
   parameters["format"] = "zip";
   this->session()->download(filename, query, parameters);
 }
+
+//----------------------------------------------------------------------------
+void ctkXnatResource::saveImpl(bool /*overwrite*/)
+{
+  ctkXnatSession::UrlParameters urlParams;
+  urlParams["xsi:type"] = this->schemaType();
+
+  const QMap<QString, QString>& properties = this->properties();
+  QMapIterator<QString, QString> itProperties(properties);
+  while (itProperties.hasNext())
+  {
+    itProperties.next();
+    if (itProperties.key() == ID || itProperties.key() == "xsiType")
+      continue;
+
+    urlParams[itProperties.key()] = itProperties.value();
+  }
+  QUuid queryId = this->session()->httpPut(this->resourceUri(), urlParams);
+  session()->httpResults(queryId, ctkXnatDefaultSchemaTypes::XSI_RESOURCE);
+}

+ 13 - 1
Libs/XNAT/Core/ctkXnatResource.h

@@ -56,11 +56,23 @@ public:
   /// Sets the label of the object.
   void setLabel(const QString& label);
 
+  void setFormat(const QString& format);
+  QString format() const;
+
+  void setTags(const QString& tags);
+  QString tags() const;
+
+  void setContent(const QString& content);
+  QString content() const;
+
   void reset();
 
-  void download(const QString& filename);
+  void saveImpl(bool overwrite);
 
   static const QString ID;
+  static const QString TAGS;
+  static const QString FORMAT;
+  static const QString CONTENT;
 
 private:
 

+ 81 - 0
Libs/XNAT/Core/ctkXnatResourceCatalogXmlParser.cpp

@@ -0,0 +1,81 @@
+/*=============================================================================
+
+  Library: XNAT/Core
+
+  Copyright (c) German Cancer Research Center,
+    Division of Medical and Biological Informatics
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=============================================================================*/
+
+#include "ctkXnatResourceCatalogXmlParser.h"
+
+#include <QDebug>
+#include <QXmlStreamReader>
+
+//----------------------------------------------------------------------------
+class ctkXnatResourceCatalogXmlParserPrivate
+{
+public:
+
+  ctkXnatResourceCatalogXmlParserPrivate()
+  {
+  }
+
+  QXmlStreamReader xmlReader;
+};
+
+ctkXnatResourceCatalogXmlParser::ctkXnatResourceCatalogXmlParser()
+  : d_ptr (new ctkXnatResourceCatalogXmlParserPrivate())
+{
+}
+ctkXnatResourceCatalogXmlParser::~ctkXnatResourceCatalogXmlParser()
+{
+  delete d_ptr;
+}
+
+void ctkXnatResourceCatalogXmlParser::setData(const QByteArray &xmlInput)
+{
+  Q_D(ctkXnatResourceCatalogXmlParser);
+  d->xmlReader.addData(xmlInput);
+}
+
+void ctkXnatResourceCatalogXmlParser::parseXml(QList<QVariantMap>& result)
+{
+  Q_D(ctkXnatResourceCatalogXmlParser);
+
+  while (!d->xmlReader.atEnd())
+  {
+    if (d->xmlReader.name().compare("entry") == 0)
+    {
+      QVariantMap map;
+      QXmlStreamAttributes attributes = d->xmlReader.attributes();
+
+      if( attributes.hasAttribute("name") && attributes.hasAttribute("digest"))
+      {
+        QString name("");
+        name += attributes.value("name");
+        QString md5("");
+        md5 += attributes.value("digest");
+        map[name] = md5;
+        result.append(map);
+      }
+    }
+    d->xmlReader.readNext();
+  }
+  if (d->xmlReader.hasError())
+  {
+    qWarning()<<"Error parsing XNAT resource catalog xml!";
+  }
+}

+ 73 - 0
Libs/XNAT/Core/ctkXnatResourceCatalogXmlParser.h

@@ -0,0 +1,73 @@
+/*=============================================================================
+
+  Library: XNAT/Core
+
+  Copyright (c) German Cancer Research Center,
+    Division of Medical and Biological Informatics
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=============================================================================*/
+
+#ifndef CTKXNATRESOURCECATALOGXMLPARSER_H
+#define CTKXNATRESOURCECATALOGXMLPARSER_H
+
+#include "ctkXNATCoreExport.h"
+
+#include <QList>
+#include <QVariantMap>
+
+class ctkXnatResourceCatalogXmlParserPrivate;
+
+/**
+ * @brief Parses the xml response when requesting the catalog xml of a resource
+ *
+ * When sending the following request to XNAT:
+ * <xnat-url>/projects/myProject/resources/<resource-id>
+ * you get the catalog xml of the resource folder as response
+ *
+ * This class parses this response and returns the MD5 hashes of all the resource files,
+ * which are contained by the resource folder.
+ *
+ * @ingroup XNAT_Core
+ */
+class CTK_XNAT_CORE_EXPORT ctkXnatResourceCatalogXmlParser
+{
+
+public:
+  ctkXnatResourceCatalogXmlParser();
+  ~ctkXnatResourceCatalogXmlParser();
+
+  /**
+   * @brief Set the xml input for the parser
+   * @param xmlInput as QByteArray
+   */
+  void setData(const QByteArray& xmlInput);
+
+  /**
+   * @brief Parses the xml input and extracts the md5 hashes of the resource catalog
+   * @param result the QList in which the md5 hashes will be stored
+   */
+  void parseXml(QList<QVariantMap> &result);
+private:
+
+  ctkXnatResourceCatalogXmlParser(const ctkXnatResourceCatalogXmlParser& );
+  ctkXnatResourceCatalogXmlParser& operator=(const ctkXnatResourceCatalogXmlParser& );
+
+  /// The private implementation part of the object.
+  ctkXnatResourceCatalogXmlParserPrivate* d_ptr;
+
+  Q_DECLARE_PRIVATE(ctkXnatResourceCatalogXmlParser)
+};
+
+#endif //CTKXNATRESOURCECATALOGXMLPARSER_H

+ 120 - 0
Libs/XNAT/Core/ctkXnatResourceFolder.cpp

@@ -0,0 +1,120 @@
+/*=============================================================================
+
+  Library: XNAT/Core
+
+  Copyright (c) University College London,
+    Centre for Medical Image Computing
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=============================================================================*/
+
+#include "ctkXnatResourceFolder.h"
+
+#include "ctkXnatDefaultSchemaTypes.h"
+#include "ctkXnatObjectPrivate.h"
+#include "ctkXnatResource.h"
+#include "ctkXnatSession.h"
+
+//----------------------------------------------------------------------------
+class ctkXnatResourceFolderPrivate : public ctkXnatObjectPrivate
+{
+public:
+
+  ctkXnatResourceFolderPrivate()
+  : ctkXnatObjectPrivate()
+  {
+  }
+
+  void reset()
+  {
+  }
+
+};
+
+
+//----------------------------------------------------------------------------
+ctkXnatResourceFolder::ctkXnatResourceFolder(ctkXnatObject* parent)
+  : ctkXnatObject(*new ctkXnatResourceFolderPrivate(), parent, QString::null)
+{
+  this->setId("resources");
+  this->setProperty(LABEL, "Resources");
+}
+
+//----------------------------------------------------------------------------
+ctkXnatResourceFolder::~ctkXnatResourceFolder()
+{
+}
+
+//----------------------------------------------------------------------------
+QString ctkXnatResourceFolder::resourceUri() const
+{
+  return QString("%1/%2").arg(parent()->resourceUri(), this->id());
+}
+
+//----------------------------------------------------------------------------
+QString ctkXnatResourceFolder::name() const
+{
+  return this->label();
+}
+
+//----------------------------------------------------------------------------
+QString ctkXnatResourceFolder::label() const
+{
+  return this->property(LABEL);
+}
+
+//----------------------------------------------------------------------------
+void ctkXnatResourceFolder::reset()
+{
+  ctkXnatObject::reset();
+}
+
+//----------------------------------------------------------------------------
+void ctkXnatResourceFolder::fetchImpl()
+{
+  QString query = this->resourceUri();
+  ctkXnatSession* const session = this->session();
+  QUuid queryId = session->httpGet(query);
+
+  QList<ctkXnatObject*> resources = session->httpResults(queryId,
+                                                           ctkXnatDefaultSchemaTypes::XSI_RESOURCE);
+
+  foreach (ctkXnatObject* resource, resources)
+  {
+    QString label = resource->name();
+    if (label.isEmpty())
+    {
+      label = "NO NAME";
+    }
+
+    resource->setName(label);
+    this->add(resource);
+  }
+}
+
+//----------------------------------------------------------------------------
+void ctkXnatResourceFolder::downloadImpl(const QString& filename)
+{
+  QString query = this->resourceUri() + "/ALL/files";
+  ctkXnatSession::UrlParameters parameters;
+  parameters["format"] = "zip";
+  this->session()->download(filename, query, parameters);
+}
+
+//----------------------------------------------------------------------------
+void ctkXnatResourceFolder::saveImpl(bool /*overwrite*/)
+{
+  // Not implemented since a resource folder is automatically created when
+  // a resource is uploaded
+}

+ 60 - 0
Libs/XNAT/Core/ctkXnatResourceFolder.h

@@ -0,0 +1,60 @@
+/*=============================================================================
+
+  Library: XNAT/Core
+
+  Copyright (c) University College London,
+    Centre for Medical Image Computing
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=============================================================================*/
+
+#ifndef ctkXnatResourceFolder_h
+#define ctkXnatResourceFolder_h
+
+#include "ctkXNATCoreExport.h"
+
+#include "ctkXnatObject.h"
+
+class ctkXnatResourceFolderPrivate;
+
+/**
+ * @ingroup XNAT_Core
+ */
+class CTK_XNAT_CORE_EXPORT ctkXnatResourceFolder : public ctkXnatObject
+{
+
+public:
+
+  ctkXnatResourceFolder(ctkXnatObject* parent = NULL);
+
+  virtual ~ctkXnatResourceFolder();
+
+  virtual QString resourceUri() const;
+
+  virtual QString name() const;
+  virtual QString label()const;
+
+  void reset();
+
+private:
+
+  friend class qRestResult;
+  virtual void fetchImpl();
+  virtual void downloadImpl(const QString&);
+  virtual void saveImpl(bool overwrite);
+
+  Q_DECLARE_PRIVATE(ctkXnatResourceFolder)
+};
+
+#endif

+ 12 - 0
Libs/XNAT/Core/ctkXnatScanFolder.cpp

@@ -66,6 +66,18 @@ QString ctkXnatScanFolder::resourceUri() const
 }
 
 //----------------------------------------------------------------------------
+QString ctkXnatScanFolder::name() const
+{
+  return this->label();
+}
+
+//----------------------------------------------------------------------------
+QString ctkXnatScanFolder::label() const
+{
+  return this->property(LABEL);
+}
+
+//----------------------------------------------------------------------------
 void ctkXnatScanFolder::reset()
 {
   ctkXnatObject::reset();

+ 3 - 0
Libs/XNAT/Core/ctkXnatScanFolder.h

@@ -44,6 +44,9 @@ public:
 
   virtual QString resourceUri() const;
 
+  virtual QString name() const;
+  virtual QString label()const;
+
   void reset();
 
 private:

+ 115 - 39
Libs/XNAT/Core/ctkXnatSession.cpp

@@ -35,8 +35,10 @@
 #include "ctkXnatScan.h"
 #include "ctkXnatSubject.h"
 
+#include <QCryptographicHash>
 #include <QDateTime>
 #include <QDebug>
+#include <QDir>
 #include <QScopedPointer>
 #include <QStringBuilder>
 #include <QNetworkCookie>
@@ -61,6 +63,7 @@ public:
   QScopedPointer<ctkXnatAPI> xnat;
   QScopedPointer<ctkXnatDataModel> dataModel;
   QString sessionId;
+  QString defaultDownloadDir;
 
   QMap<QString, QString> sessionProperties;
 
@@ -87,6 +90,7 @@ ctkXnatSessionPrivate::ctkXnatSessionPrivate(const ctkXnatLoginProfile& loginPro
                                              ctkXnatSession* q)
   : loginProfile(loginProfile)
   , xnat(new ctkXnatAPI())
+  , defaultDownloadDir(".")
   , q(q)
 {
   // TODO This is a workaround for connecting to sites with self-signed
@@ -329,6 +333,12 @@ ctkXnatSession::ctkXnatSession(const ctkXnatLoginProfile& loginProfile)
   QString url = d->loginProfile.serverUrl().toString();
   d->xnat->setServerUrl(url);
 
+//  QObject::connect(d->xnat.data(), SIGNAL(uploadFinished()), this, SIGNAL(uploadFinished()));
+  QObject::connect(d->xnat.data(), SIGNAL(progress(QUuid,double)),
+          this, SIGNAL(progress(QUuid,double)));
+//  QObject::connect(d->xnat.data(), SIGNAL(progress(QUuid,double)),
+//          this, SLOT(onProgress(QUuid,double)));
+
   d->setDefaultHttpHeaders();
 }
 
@@ -432,7 +442,7 @@ ctkXnatLoginProfile ctkXnatSession::loginProfile() const
 }
 
 //----------------------------------------------------------------------------
-void ctkXnatSession::progress(QUuid /*queryId*/, double /*progress*/)
+void ctkXnatSession::onProgress(QUuid /*queryId*/, double /*progress*/)
 {
 //  qDebug() << "ctkXnatSession::progress(QUuid queryId, double progress)";
 //  qDebug() << "query id:" << queryId;
@@ -468,6 +478,30 @@ QString ctkXnatSession::sessionId() const
 }
 
 //----------------------------------------------------------------------------
+void ctkXnatSession::setDefaultDownloadDir(const QString &path)
+{
+  Q_D(ctkXnatSession);
+
+  QDir directory(path);
+  if (directory.exists())
+  {
+    d->defaultDownloadDir = path;
+  }
+  else
+  {
+    d->defaultDownloadDir = QDir::currentPath();
+    qWarning() << "Specified directory: ["<<path<<"] does not exists! Setting default filepath to :"<<d->defaultDownloadDir;
+  }
+}
+
+//----------------------------------------------------------------------------
+QString ctkXnatSession::defaultDownloadDir() const
+{
+  Q_D(const ctkXnatSession);
+  return d->defaultDownloadDir;
+}
+
+//----------------------------------------------------------------------------
 ctkXnatDataModel* ctkXnatSession::dataModel() const
 {
   Q_D(const ctkXnatSession);
@@ -497,6 +531,14 @@ QList<ctkXnatObject*> ctkXnatSession::httpResults(const QUuid& uuid, const QStri
   return d->results(restResult.data(), schemaType);
 }
 
+QUuid ctkXnatSession::httpPut(const QString& resource, const ctkXnatSession::UrlParameters& parameters,
+                              const ctkXnatSession::HttpRawHeaders& /*rawHeaders*/)
+{
+  Q_D(ctkXnatSession);
+  d->checkSession();
+  return d->xnat->put(resource, parameters);
+}
+
 //----------------------------------------------------------------------------
 QList<QVariantMap> ctkXnatSession::httpSync(const QUuid& uuid)
 {
@@ -518,17 +560,6 @@ QList<QVariantMap> ctkXnatSession::httpSync(const QUuid& uuid)
 }
 
 //----------------------------------------------------------------------------
-bool ctkXnatSession::exists(const ctkXnatObject* object)
-{
-  Q_D(ctkXnatSession);
-
-  QString query = object->resourceUri();
-  bool success = d->xnat->sync(d->xnat->get(query));
-
-  return success;
-}
-
-//----------------------------------------------------------------------------
 const QMap<QByteArray, QByteArray> ctkXnatSession::httpHeadSync(const QUuid &uuid)
 {
   Q_D(ctkXnatSession);
@@ -549,38 +580,14 @@ QUuid ctkXnatSession::httpHead(const QString& resourceUri)
 }
 
 //----------------------------------------------------------------------------
-void ctkXnatSession::save(ctkXnatObject* object)
+bool ctkXnatSession::exists(const ctkXnatObject* object)
 {
   Q_D(ctkXnatSession);
 
   QString query = object->resourceUri();
-  query.append(QString("?%1=%2").arg("xsi:type", object->schemaType()));
-  const QMap<QString, QString>& properties = object->properties();
-  QMapIterator<QString, QString> itProperties(properties);
-  while (itProperties.hasNext())
-  {
-    itProperties.next();
-    query.append(QString("&%1=%2").arg(itProperties.key(), itProperties.value()));
-  }
-
-  qDebug() << "ctkXnatSession::save() query:" << query;
-  QUuid queryId = d->xnat->put(query);
-  qRestResult* result = d->xnat->takeResult(queryId);
-
-  if (!result || !result->error().isNull())
-  {
-    d->throwXnatException("Error occurred while creating the data.");
-  }
+  bool success = d->xnat->sync(d->xnat->get(query));
 
-  const QList<QVariantMap>& maps = result->results();
-  if (maps.size() == 1 && maps[0].size() == 1)
-  {
-    QVariant id = maps[0][ctkXnatObject::ID];
-    if (!id.isNull())
-    {
-      object->setId(id.toString());
-    }
-  }
+  return success;
 }
 
 //----------------------------------------------------------------------------
@@ -610,6 +617,75 @@ void ctkXnatSession::download(const QString& fileName,
 }
 
 //----------------------------------------------------------------------------
+void ctkXnatSession::upload(ctkXnatFile *xnatFile,
+                            const UrlParameters &parameters,
+                            const HttpRawHeaders &/*rawHeaders*/)
+{
+  Q_D(ctkXnatSession);
+
+  QFile file(xnatFile->localFilePath());
+
+  if (!file.exists())
+  {
+    QString msg = "Error uploading file! ";
+    msg.append(QString("File \"%1\" does not exist!").arg(xnatFile->localFilePath()));
+    throw ctkXnatException(msg);
+  }
+
+  QUuid queryId = d->xnat->upload(xnatFile->localFilePath(), xnatFile->resourceUri(), parameters);
+  d->xnat->sync(queryId);
+
+  // Validating the file upload by requesting the catalog XML
+  // of the parent resource. Unfortunately for XNAT versions <= 1.6.4
+  // this is the only way to get the file's MD5 hash form the server.
+  QString md5Query = xnatFile->parent()->resourceUri();
+  QUuid md5QueryID = this->httpGet(md5Query);
+  QList<QVariantMap> result = this->httpSync(md5QueryID);
+
+  QString md5ChecksumRemote ("0");
+  // Newly added files are usually at the end of the catalog
+  // and hence at the end of the result list.
+  // So iterating backward is for performance reasons.
+  QList<QVariantMap>::const_iterator it = result.constEnd()-1;
+  while (it != result.constBegin()-1)
+  {
+    QVariantMap::const_iterator it2 = (*it).find(xnatFile->name());
+    if (it2 != (*it).constEnd())
+    {
+      md5ChecksumRemote = it2.value().toString();
+      break;
+    }
+    --it;
+  }
+
+  QFile localFile(xnatFile->localFilePath());
+  if (localFile.open(QFile::ReadOnly) && md5ChecksumRemote != "0")
+  {
+    QCryptographicHash hash(QCryptographicHash::Md5);
+
+#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
+    hash.addData(&localFile);
+#else
+    hash.addData(localFile.readAll());
+#endif
+
+    QString md5ChecksumLocal(hash.result().toHex());
+    // Retrieving the md5 checksum on the server and comparing
+    // it with the local file md5 sum
+    if (md5ChecksumLocal != md5ChecksumRemote)
+    {
+      // Remove corrupted file from server
+      xnatFile->erase();
+      throw ctkXnatException("Upload failed! An error occurred during file upload.");
+    }
+  }
+  else
+  {
+    qWarning()<<"Could not validate file upload! Remote MD5: "<<md5ChecksumRemote;
+  }
+}
+
+//----------------------------------------------------------------------------
 void ctkXnatSession::processResult(QUuid queryId, QList<QVariantMap> parameters)
 {
   Q_UNUSED(queryId)

+ 45 - 2
Libs/XNAT/Core/ctkXnatSession.h

@@ -146,6 +146,24 @@ public:
    */
   QString sessionId() const;
 
+  /**
+    * @brief Sets the default location where files will be saved after being downloaded
+    *
+    * Sets the default directory into which file downloads will be saved.
+    * By default this value is empty and files will be stored into the current
+    * working directory.
+    * If the path does not exists a warning will be printed and the path will be
+    * set to the current working directory.
+    *
+    * @param path the path to the download location
+    */
+  void setDefaultDownloadDir(const QString& path);
+
+  /**
+    * @brief returns the default download location
+    * @return the default download directory as string
+    */
+  QString defaultDownloadDir() const;
 
   ctkXnatDataModel* dataModel() const;
 
@@ -175,6 +193,18 @@ public:
   /**
    * @brief TODO
    * @param uuid
+   * @param parameters
+   *
+   * @throws ctkXnatInvalidSessionException if the session is closed.
+   * @return
+   */
+   QUuid httpPut(const QString& resource,
+                const UrlParameters& parameters = UrlParameters(),
+                const HttpRawHeaders& rawHeaders = HttpRawHeaders());
+
+  /**
+   * @brief TODO
+   * @param uuid
    *
    * @throws ctkXnatInvalidSessionException if the session is closed.
    * @return
@@ -192,7 +222,6 @@ public:
 
   bool exists(const ctkXnatObject* object);
 
-  void save(ctkXnatObject* object);
   void remove(ctkXnatObject* object);
 
   /// Downloads a file from the web service.
@@ -206,6 +235,16 @@ public:
     const UrlParameters& parameters = UrlParameters(),
     const HttpRawHeaders& rawHeaders = HttpRawHeaders());
 
+  /// Uploads a file to the server.
+  /// \a fileName is the name of the file.
+  /// The \a resource and \parameters are used to compose the URL.
+  /// \a rawHeaders can be used to set the raw headers of the request to send.
+  /// These headers will be set additionally to those defined by the
+  /// \a defaultRawHeaders property.
+  void upload(ctkXnatFile *xnatFile,
+    const UrlParameters& parameters = UrlParameters(),
+    const HttpRawHeaders& rawHeaders = HttpRawHeaders());
+
   /**
    * @brief Sends a http HEAD request to the xnat instance
    * @param resourceUri the URL to the server
@@ -229,9 +268,13 @@ public:
    */
   Q_SIGNAL void sessionAboutToBeClosed();
 
+//  Q_SIGNAL void uploadFinished();
+
+  Q_SIGNAL void progress(QUuid, double);
+
 public slots:
   void processResult(QUuid queryId, QList<QVariantMap> parameters);
-  void progress(QUuid queryId, double progress);
+  void onProgress(QUuid queryId, double onProgress);
 
 protected:
   QScopedPointer<ctkXnatSessionPrivate> d_ptr;

+ 89 - 10
Libs/XNAT/Core/ctkXnatTreeModel.cpp

@@ -182,7 +182,7 @@ bool ctkXnatTreeModel::hasChildren(const QModelIndex& index) const
   }
 
   ctkXnatTreeItem* item = d->itemAt(index);
-  return !item->xnatObject()->isFetched() || !item->xnatObject()->children().isEmpty();
+  return item->xnatObject() || !item->xnatObject()->isFetched() || !item->xnatObject()->children().isEmpty();
 }
 
 //----------------------------------------------------------------------------
@@ -226,6 +226,88 @@ void ctkXnatTreeModel::fetchMore(const QModelIndex& index)
 }
 
 //----------------------------------------------------------------------------
+void ctkXnatTreeModel::refresh(const QModelIndex& parent)
+{
+  if (!parent.isValid())
+  {
+    return;
+  }
+
+  Q_D(const ctkXnatTreeModel);
+
+  ctkXnatTreeItem* item = d->itemAt(parent);
+
+  ctkXnatObject* xnatObject = item->xnatObject();
+
+  // Do this just for xnatObjects that are already fetched.
+  // Otherwise we would retrieve all data from XNAT
+  if (xnatObject->isFetched())
+  {
+    // Force a fetch for current object (it might has changed on the server)
+    xnatObject->fetch(true);
+
+    int numChildren = rowCount(parent);
+
+    bool addToTreeView (true);
+
+    // For all children, check if they are already in the treeview,
+    // if not -> add them
+    // For all items of the treeview, check if they are still on the server
+    // if not -> remove them
+    QList<ctkXnatObject*> children = xnatObject->children();
+    QMutableListIterator<ctkXnatObject*> iter (children);
+
+    while (iter.hasNext())
+    {
+      ctkXnatObject* child = iter.next();
+      for (int i = 0; i < numChildren; ++i)
+      {
+        ctkXnatObject* childItemObject = item->child(i)->xnatObject();
+
+        // If the item was deleted from the server in the meantime
+        // -> remove it from the treeview
+        if (!childItemObject->exists())
+        {
+          beginRemoveRows(parent, item->child(i)->row(), item->child(i)->row());
+          item->remove(childItemObject);
+          xnatObject->remove(child);
+          iter.remove();
+          endRemoveRows();
+          --numChildren;
+          --i;
+          addToTreeView = false;
+          break;
+        }
+
+        if ((childItemObject->id().length() != 0 && childItemObject->id() == child->id()) ||
+            (childItemObject->id().length() == 0 && childItemObject->name() == child->name()))
+        {
+          addToTreeView = false;
+          break;
+        }
+      }
+
+      // If the current xnatObject was created on the server in the meantime
+      // -> add it to the treeview
+      if (addToTreeView)
+      {
+        beginInsertRows(parent, 0, numChildren - 1);
+        item->appendChild(new ctkXnatTreeItem(child, item));
+        endInsertRows();
+        ++numChildren;
+      }
+      addToTreeView = true;
+    }
+
+    numChildren = rowCount(parent);
+    for (int i=0; i<numChildren; i++)
+    {
+      refresh(index(i,0,parent));
+    }
+  }
+}
+
+//----------------------------------------------------------------------------
 ctkXnatObject* ctkXnatTreeModel::xnatObject(const QModelIndex& index) const
 {
   Q_D(const ctkXnatTreeModel);
@@ -298,15 +380,12 @@ void ctkXnatTreeModel::downloadFile(const QModelIndex& index, const QString& zip
 }
 
 //----------------------------------------------------------------------------
-void ctkXnatTreeModel::uploadFile(const QModelIndex& index, const QString& zipFileName)
+void ctkXnatTreeModel::addChildNode(const QModelIndex &index, ctkXnatObject* child)
 {
-  if (!index.isValid())
-  {
-    return;
-  }
-
-  ctkXnatObject* xnatObject = this->xnatObject(index);
-  ctkXnatObject* child = xnatObject->children()[index.row()];
+  Q_D(ctkXnatTreeModel);
+  ctkXnatTreeItem* item = d->itemAt(index);
 
-  child->upload(zipFileName);
+  beginInsertRows(index, 0, 1);
+  item->appendChild(new ctkXnatTreeItem(child, item));
+  endInsertRows();
 }

+ 8 - 1
Libs/XNAT/Core/ctkXnatTreeModel.h

@@ -50,6 +50,12 @@ public:
   virtual bool canFetchMore(const QModelIndex& parent) const;
   virtual void fetchMore(const QModelIndex& parent);
 
+  /**
+   * @brief Convenience method for refreshing an entry of the tree model
+   * @param The parent item, whose children will be refreshed
+   */
+  virtual void refresh(const QModelIndex& parent = QModelIndex());
+
   ctkXnatObject* xnatObject(const QModelIndex& index) const;
 
   void addDataModel(ctkXnatDataModel* dataModel);
@@ -57,9 +63,10 @@ public:
 
   bool removeAllRows(const QModelIndex& parent);
 
-  void uploadFile(const QModelIndex& index, const QString& zipFilename);
   void downloadFile (const QModelIndex& index, const QString& zipFilename);
 
+  void addChildNode(const QModelIndex& index, ctkXnatObject *child);
+
 private:
 
   const QScopedPointer<ctkXnatTreeModelPrivate> d_ptr;

+ 1 - 0
Libs/XNAT/Widgets/ctkXnatLoginDialog.cpp

@@ -212,6 +212,7 @@ void ctkXnatLoginDialog::accept()
     return;
     }
   d->Session = session.take();
+  d->Session->setDefaultDownloadDir(ui->edtDownloadDir->text());
 
   QDialog::accept();
 }

+ 11 - 2
Libs/XNAT/Widgets/ctkXnatLoginDialog.ui

@@ -156,13 +156,23 @@
            </property>
           </widget>
          </item>
-         <item row="5" column="1">
+         <item row="5" column="0">
+          <widget class="QLabel" name="lblDownloadDir">
+           <property name="text">
+            <string>Download Dir.:</string>
+           </property>
+          </widget>
+         </item>
+         <item row="6" column="1">
           <widget class="QCheckBox" name="cbxDefaultProfile">
            <property name="text">
             <string>Use as default</string>
            </property>
           </widget>
          </item>
+         <item row="5" column="1">
+          <widget class="QLineEdit" name="edtDownloadDir"/>
+         </item>
         </layout>
        </widget>
       </item>
@@ -186,7 +196,6 @@
   <tabstop>edtServerUri</tabstop>
   <tabstop>edtUserName</tabstop>
   <tabstop>edtPassword</tabstop>
-  <tabstop>cbxDefaultProfile</tabstop>
   <tabstop>btnSave</tabstop>
   <tabstop>btnDelete</tabstop>
   <tabstop>lstProfiles</tabstop>