Bläddra i källkod

Added http session support.

Sascha Zelzer 12 år sedan
förälder
incheckning
a0da58d91c

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

@@ -30,6 +30,7 @@ set(KIT_SRCS
 # Files which should be processed by Qts moc
 set(KIT_MOC_SRCS
   ctkXnatAPI_p.h
+  ctkXnatSession.h
 )
 
 

+ 70 - 0
Libs/XNAT/Core/Testing/ctkXnatSessionTest.cpp

@@ -36,6 +36,7 @@
 #include <ctkXnatSession.h>
 #include <ctkXnatProject.h>
 #include <ctkXnatSubject.h>
+#include <ctkXnatException.h>
 
 class ctkXnatSessionTestCasePrivate
 {
@@ -47,6 +48,8 @@ public:
   QString Project;
   QString Subject;
   QString Experiment;
+
+  QDateTime DateTime;
 };
 
 // --------------------------------------------------------------------------
@@ -69,15 +72,28 @@ void ctkXnatSessionTestCase::initTestCase()
   d->LoginProfile.setServerUrl(QString("https://central.xnat.org"));
   d->LoginProfile.setUserName("ctk");
   d->LoginProfile.setPassword("ctk");
+}
 
+void ctkXnatSessionTestCase::init()
+{
+  Q_D(ctkXnatSessionTestCase);
+
+  d->DateTime = QDateTime::currentDateTime();
+  qDebug() << "d->DateTime " << d->DateTime;
   d->Session = new ctkXnatSession(d->LoginProfile);
+  d->Session->open();
 }
 
 void ctkXnatSessionTestCase::cleanupTestCase()
 {
+}
+
+void ctkXnatSessionTestCase::cleanup()
+{
   Q_D(ctkXnatSessionTestCase);
 
   delete d->Session;
+  d->Session = NULL;
 }
 
 void ctkXnatSessionTestCase::testProjectList()
@@ -138,6 +154,60 @@ void ctkXnatSessionTestCase::testParentChild()
   delete project;
 }
 
+void ctkXnatSessionTestCase::testSession()
+{
+  Q_D(ctkXnatSessionTestCase);
+
+  QVERIFY(d->Session->isOpen());
+  QVERIFY(d->Session->version() == "1.6.1");
+  QDateTime expirationDate = d->Session->expirationDate();
+
+  QVERIFY(d->DateTime < expirationDate);
+
+  QTest::qSleep(2000);
+
+  QUuid uuid = d->Session->httpGet("/data/version");
+  QVERIFY(!uuid.isNull());
+  d->Session->httpSync(uuid);
+
+  QVERIFY(expirationDate < d->Session->expirationDate());
+
+  try
+  {
+    d->Session->httpSync(uuid);
+    QFAIL("Exception for unknown uuid expected");
+  }
+  catch(const ctkInvalidArgumentException&)
+  {}
+
+  d->Session->close();
+  try
+  {
+    d->Session->dataModel();
+    QFAIL("Exception for closed session expected");
+  }
+  catch(const ctkXnatSessionException&)
+  {}
+}
+
+void ctkXnatSessionTestCase::testAuthenticationError()
+{
+  ctkXnatLoginProfile loginProfile;
+  loginProfile.setName("error");
+  loginProfile.setServerUrl(QString("https://central.xnat.org"));
+  loginProfile.setUserName("x");
+  loginProfile.setPassword("y");
+
+  ctkXnatSession session(loginProfile);
+  try
+  {
+    session.open();
+    QFAIL("Authenication error exception expected");
+  }
+  catch (const ctkXnatAuthenticationException&)
+  {}
+}
+
 void ctkXnatSessionTestCase::testCreateProject()
 {
   Q_D(ctkXnatSessionTestCase);

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

@@ -40,10 +40,18 @@ private slots:
 
   void initTestCase();
 
+  void init();
+
   void cleanupTestCase();
 
+  void cleanup();
+
   void testParentChild();
 
+  void testSession();
+
+  void testAuthenticationError();
+
   void testProjectList();
 
   void testResourceUri();

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

@@ -85,11 +85,14 @@ void ctkXnatAPI::parseResponse(qRestResult* restResult, const QByteArray& respon
     // E.g. creating a subject.
     QVariantMap map;
     map["ID"] = response;
+    map["content"] = response;
     result.push_back(map);
     }
   else
     {
-    restResult->setError(QString("Bad data: ") + response, qRestAPI::ResponseParseError);
+    QVariantMap map;
+    map["content"] = response;
+    result.push_back(map);
     }
 
   restResult->setResult(result);

+ 3 - 2
Libs/XNAT/Core/ctkXnatException.cpp

@@ -22,5 +22,6 @@
 #include "ctkXnatException.h"
 
 CTK_IMPLEMENT_EXCEPTION(ctkXnatTimeoutException, ctkRuntimeException, "ctkXnatTimeoutException")
-CTK_IMPLEMENT_EXCEPTION(ctkXnatSessionExpiredException, ctkRuntimeException, "ctkXnatSessionExpiredException")
-CTK_IMPLEMENT_EXCEPTION(ctkXnatResponseParseError, ctkRuntimeException, "ctkXnatResponseParseError")
+CTK_IMPLEMENT_EXCEPTION(ctkXnatSessionException, ctkRuntimeException, "ctkXnatSessionException")
+CTK_IMPLEMENT_EXCEPTION(ctkXnatAuthenticationException, ctkXnatSessionException, "ctkXnatAuthenticationException")
+CTK_IMPLEMENT_EXCEPTION(ctkXnatProtocolFailureException, ctkRuntimeException, "ctkXnatProtocolFailureException")

+ 3 - 2
Libs/XNAT/Core/ctkXnatException.h

@@ -27,7 +27,8 @@
 #include "ctkException.h"
 
 CTK_DECLARE_EXCEPTION(CTK_XNAT_CORE_EXPORT, ctkXnatTimeoutException, ctkRuntimeException)
-CTK_DECLARE_EXCEPTION(CTK_XNAT_CORE_EXPORT, ctkXnatSessionExpiredException, ctkRuntimeException)
-CTK_DECLARE_EXCEPTION(CTK_XNAT_CORE_EXPORT, ctkXnatResponseParseError, ctkRuntimeException)
+CTK_DECLARE_EXCEPTION(CTK_XNAT_CORE_EXPORT, ctkXnatSessionException, ctkRuntimeException)
+CTK_DECLARE_EXCEPTION(CTK_XNAT_CORE_EXPORT, ctkXnatAuthenticationException, ctkXnatSessionException)
+CTK_DECLARE_EXCEPTION(CTK_XNAT_CORE_EXPORT, ctkXnatProtocolFailureException, ctkRuntimeException)
 
 #endif

+ 222 - 17
Libs/XNAT/Core/ctkXnatSession.cpp

@@ -39,17 +39,31 @@
 #include <QDebug>
 #include <QScopedPointer>
 #include <QStringBuilder>
+#include <QNetworkCookie>
+#include <QDateTime>
 
 #include <ctkXnatAPI_p.h>
 #include <qRestResult.h>
 
+static const char* HEADER_AUTHORIZATION = "Authorization";
+static const char* HEADER_USER_AGENT = "User-Agent";
+static const char* HEADER_COOKIE = "Cookie";
+
+static QString SERVER_VERSION = "version";
+static QString SESSION_EXPIRATION_DATE = "expires";
+
 class ctkXnatSessionPrivate
 {
 public:
   const ctkXnatLoginProfile loginProfile;
 
-  ctkXnatAPI* xnat;
-  ctkXnatDataModel* dataModel;
+  QScopedPointer<ctkXnatAPI> xnat;
+  QScopedPointer<ctkXnatDataModel> dataModel;
+  QString sessionId;
+
+  QMap<QString, QString> sessionProperties;
+
+  ctkXnatSession* q;
 
   ctkXnatSessionPrivate(const ctkXnatLoginProfile& loginProfile, ctkXnatSession* q);
   ~ctkXnatSessionPrivate();
@@ -57,12 +71,19 @@ public:
   void throwXnatException(const QString& msg);
 
   void createConnections();
+  void setDefaultHttpHeaders();
+  void checkSession() const;
+  void setSessionProperties();
+  QDateTime updateExpirationDate(qRestResult* restResult);
+
+  void close();
 };
 
-ctkXnatSessionPrivate::ctkXnatSessionPrivate(const ctkXnatLoginProfile& loginProfile, ctkXnatSession* q)
+ctkXnatSessionPrivate::ctkXnatSessionPrivate(const ctkXnatLoginProfile& loginProfile,
+                                             ctkXnatSession* q)
   : loginProfile(loginProfile)
   , xnat(new ctkXnatAPI())
-  , dataModel(new ctkXnatDataModel(q))
+  , q(q)
 {
   // TODO This is a workaround for connecting to sites with self-signed
   // certificate. Should be replaced with something more clever.
@@ -73,8 +94,6 @@ ctkXnatSessionPrivate::ctkXnatSessionPrivate(const ctkXnatLoginProfile& loginPro
 
 ctkXnatSessionPrivate::~ctkXnatSessionPrivate()
 {
-  delete dataModel;
-  delete xnat;
 }
 
 void ctkXnatSessionPrivate::throwXnatException(const QString& msg)
@@ -91,7 +110,14 @@ void ctkXnatSessionPrivate::throwXnatException(const QString& msg)
   case qRestAPI::TimeoutError:
     throw ctkXnatTimeoutException(errorMsg);
   case qRestAPI::ResponseParseError:
-    throw ctkXnatResponseParseError(errorMsg);
+    throw ctkXnatProtocolFailureException(errorMsg);
+  case qRestAPI::UnknownUuidError:
+    throw ctkInvalidArgumentException(errorMsg);
+  case qRestAPI::AuthenticationError:
+    // This signals either an initial authentication error
+    // or a session timeout.
+    this->close();
+    throw ctkXnatAuthenticationException(errorMsg);
   default:
     throw ctkRuntimeException(errorMsg);
   }
@@ -106,6 +132,91 @@ void ctkXnatSessionPrivate::createConnections()
   //           this, SLOT(progress(QUuid,double)));
 }
 
+void ctkXnatSessionPrivate::setDefaultHttpHeaders()
+{
+  ctkXnatAPI::RawHeaders rawHeaders;
+  rawHeaders[HEADER_USER_AGENT] = "Qt";
+  /*
+  rawHeaders["Authorization"] = "Basic " +
+      QByteArray(QString("%1:%2").arg(d->loginProfile.userName())
+                 .arg(d->loginProfile.password()).toAscii()).toBase64();
+  */
+  if (!sessionId.isEmpty())
+  {
+    rawHeaders[HEADER_COOKIE] = QString("JSESSIONID=%1").arg(sessionId).toAscii();
+  }
+  xnat->setDefaultRawHeaders(rawHeaders);
+}
+
+void ctkXnatSessionPrivate::checkSession() const
+{
+  if (sessionId.isEmpty())
+  {
+    throw ctkXnatSessionException("Session closed.");
+  }
+}
+
+void ctkXnatSessionPrivate::setSessionProperties()
+{
+  sessionProperties.clear();
+  QUuid uuid = xnat->get("/data/version");
+  QScopedPointer<qRestResult> restResult(xnat->takeResult(uuid));
+  if (restResult)
+  {
+    QString version = restResult->result()["content"].toString();
+    if (version.isEmpty())
+    {
+      throw ctkXnatProtocolFailureException("No version information available.");
+    }
+    sessionProperties[SERVER_VERSION] = version;
+  }
+  else
+  {
+    this->throwXnatException("Retrieving session properties failed.");
+  }
+}
+
+QDateTime ctkXnatSessionPrivate::updateExpirationDate(qRestResult* restResult)
+{
+  QByteArray cookieHeader = restResult->rawHeader("Set-Cookie");
+  QDateTime expirationDate = QDateTime::currentDateTime();
+  if (!cookieHeader.isEmpty())
+  {
+    QList<QNetworkCookie> cookies = QNetworkCookie::parseCookies(cookieHeader);
+    foreach(const QNetworkCookie& cookie, cookies)
+    {
+      if (cookie.name() == "SESSION_EXPIRATION_TIME")
+      {
+        QList<QByteArray> expirationCookie = cookie.value().split(',');
+        if (expirationCookie.size() == 2)
+        {
+          unsigned long long startTime = expirationCookie[0].mid(1).toULongLong();
+          if (startTime > 0)
+          {
+            expirationDate = QDateTime::fromTime_t(startTime / 1000);
+          }
+          QByteArray timeSpan = expirationCookie[1];
+          timeSpan.chop(1);
+          expirationDate = expirationDate.addMSecs(timeSpan.toLong());
+          sessionProperties[SESSION_EXPIRATION_DATE] = expirationDate.toString(Qt::ISODate);
+          emit q->sessionRenewed(expirationDate);
+        }
+      }
+    }
+  }
+  qDebug() << "NEW EXPIR " << expirationDate;
+  return expirationDate;
+}
+
+void ctkXnatSessionPrivate::close()
+{
+  sessionProperties.clear();
+  sessionId.clear();
+  this->setDefaultHttpHeaders();
+
+  dataModel.reset();
+}
+
 // ctkXnatSession class
 
 ctkXnatSession::ctkXnatSession(const ctkXnatLoginProfile& loginProfile)
@@ -123,21 +234,93 @@ ctkXnatSession::ctkXnatSession(const ctkXnatLoginProfile& loginProfile)
   qRegisterMetaType<ctkXnatFile>(ctkXnatFile::staticSchemaType());
   qRegisterMetaType<ctkXnatReconstructionResource>(ctkXnatReconstructionResource::staticSchemaType());
 
-  ctkXnatAPI::RawHeaders rawHeaders;
-  rawHeaders["User-Agent"] = "Qt";
-  rawHeaders["Authorization"] = "Basic " +
-      QByteArray(QString("%1:%2").arg(d->loginProfile.userName())
-                 .arg(d->loginProfile.password()).toAscii()).toBase64();
-  d->xnat->setDefaultRawHeaders(rawHeaders);
-
   QString url = d->loginProfile.serverUrl().toString();
   d->xnat->setServerUrl(url);
 
-  d->dataModel->setProperty("ID", url);
+  d->setDefaultHttpHeaders();
 }
 
 ctkXnatSession::~ctkXnatSession()
 {
+  this->close();
+}
+
+void ctkXnatSession::open()
+{
+  Q_D(ctkXnatSession);
+
+  if (this->isOpen()) return;
+
+  qRestAPI::RawHeaders headers;
+  headers[HEADER_AUTHORIZATION] = "Basic " +
+                                  QByteArray(QString("%1:%2").arg(this->userName())
+                                             .arg(this->password()).toAscii()).toBase64();
+  QUuid uuid = d->xnat->get("/data/JSESSION", qRestAPI::Parameters(), headers);
+  QScopedPointer<qRestResult> restResult(d->xnat->takeResult(uuid));
+  if (restResult)
+  {
+    QString sessionId = restResult->result()["content"].toString();
+    d->sessionId = sessionId;
+    d->setDefaultHttpHeaders();
+    d->setSessionProperties();
+    d->updateExpirationDate(restResult.data());
+  }
+  else
+  {
+    d->throwXnatException("Could not get a session id.");
+  }
+
+  d->dataModel.reset(new ctkXnatDataModel(this));
+  d->dataModel->setProperty("ID", this->url().toString());
+}
+
+void ctkXnatSession::close()
+{
+  Q_D(ctkXnatSession);
+
+  if (!this->isOpen()) return;
+
+  d->close();
+}
+
+bool ctkXnatSession::isOpen() const
+{
+  Q_D(const ctkXnatSession);
+  return !d->sessionId.isEmpty();
+}
+
+QString ctkXnatSession::version() const
+{
+  Q_D(const ctkXnatSession);
+  if (d->sessionProperties.contains(SERVER_VERSION))
+  {
+    return d->sessionProperties[SERVER_VERSION];
+  }
+  else
+  {
+    return QString::null;
+  }
+}
+
+QDateTime ctkXnatSession::expirationDate() const
+{
+  Q_D(const ctkXnatSession);
+  d->checkSession();
+  return QDateTime::fromString(d->sessionProperties[SESSION_EXPIRATION_DATE], Qt::ISODate);
+}
+
+QDateTime ctkXnatSession::renew()
+{
+  Q_D(ctkXnatSession);
+  d->checkSession();
+
+  QUuid uuid = d->xnat->get("/data/auth");
+  QScopedPointer<qRestResult> restResult(d->xnat->takeResult(uuid));
+  if (!restResult)
+  {
+    d->throwXnatException("Session renewal failed.");
+  }
+  return d->updateExpirationDate(restResult.data());
 }
 
 ctkXnatLoginProfile ctkXnatSession::loginProfile() const
@@ -174,27 +357,49 @@ QString ctkXnatSession::password() const
 ctkXnatDataModel* ctkXnatSession::dataModel() const
 {
   Q_D(const ctkXnatSession);
-  return d->dataModel;
+  d->checkSession();
+  return d->dataModel.data();
 }
 
 QUuid ctkXnatSession::httpGet(const QString& resource, const ctkXnatSession::UrlParameters& parameters, const ctkXnatSession::HttpRawHeaders& rawHeaders)
 {
   Q_D(ctkXnatSession);
+  d->checkSession();
   return d->xnat->get(resource, parameters, rawHeaders);
 }
 
 QList<ctkXnatObject*> ctkXnatSession::httpResults(const QUuid& uuid, const QString& schemaType)
 {
   Q_D(ctkXnatSession);
+  d->checkSession();
 
   QScopedPointer<qRestResult> restResult(d->xnat->takeResult(uuid));
   if (restResult == NULL)
   {
-    d->throwXnatException("REST result is NULL");
+    d->throwXnatException("Http request failed.");
   }
   return restResult->results<ctkXnatObject>(schemaType);
 }
 
+QList<QVariantMap> ctkXnatSession::httpSync(const QUuid& uuid)
+{
+  Q_D(ctkXnatSession);
+  d->checkSession();
+
+  QList<QVariantMap> result;
+  qRestResult* restResult = d->xnat->takeResult(uuid);
+  if (restResult == NULL)
+  {
+    d->throwXnatException("Syncing with http request failed.");
+  }
+  else
+  {
+    d->updateExpirationDate(restResult);
+    result = restResult->results();
+  }
+  return result;
+}
+
 bool ctkXnatSession::exists(const ctkXnatObject* object)
 {
   Q_D(ctkXnatSession);

+ 18 - 1
Libs/XNAT/Core/ctkXnatSession.h

@@ -31,6 +31,8 @@
 #include <QVariantMap>
 #include <QUuid>
 
+class QDateTime;
+
 class ctkXnatSessionPrivate;
 
 class ctkXnatFile;
@@ -40,8 +42,9 @@ class ctkXnatObject;
 class ctkXnatScanResource;
 class ctkXnatReconstructionResource;
 
-class CTK_XNAT_CORE_EXPORT ctkXnatSession
+class CTK_XNAT_CORE_EXPORT ctkXnatSession : public QObject
 {
+  Q_OBJECT
 
 public:
 
@@ -51,6 +54,16 @@ public:
   ctkXnatSession(const ctkXnatLoginProfile& loginProfile);
   ~ctkXnatSession();
 
+  void open();
+  void close();
+
+  bool isOpen() const;
+
+  QString version() const;
+
+  QDateTime expirationDate() const;
+  QDateTime renew();
+
   /**
    * @brief Get the current login profile for this session object.
    * @return A copy of the currently used login profile.
@@ -92,6 +105,8 @@ public:
 
   QList<ctkXnatObject*> httpResults(const QUuid& uuid, const QString& schemaType);
 
+  QList<QVariantMap> httpSync(const QUuid& uuid);
+
   bool exists(const ctkXnatObject* object);
 
   void save(ctkXnatObject* object);
@@ -113,6 +128,8 @@ public:
 
 //  void download(ctkXnatReconstructionResourceFile* reconstructionResourceFile, const QString& zipFileName);
 
+  Q_SIGNAL void sessionRenewed(const QDateTime& expirationDate);
+
 public slots:
   void processResult(QUuid queryId, QList<QVariantMap> parameters);
   void progress(QUuid queryId, double progress);