# HG changeset patch # User Sebastien Jodogne # Date 1404985126 -7200 # Node ID 1fc112c4b832eab4c0a356b631abde6074a134c0 # Parent f4bbf13572cdbbf66efd0fa2484c3857830b59bd# Parent cd8569f7dd2169aa06d40dfef6e7c9b0cd82a5b2 integration lua-scripting->mainline diff -r f4bbf13572cd -r 1fc112c4b832 CMakeLists.txt --- a/CMakeLists.txt Thu Jul 10 11:26:05 2014 +0200 +++ b/CMakeLists.txt Thu Jul 10 11:38:46 2014 +0200 @@ -94,6 +94,7 @@ Core/MultiThreading/BagOfRunnablesBySteps.cpp Core/MultiThreading/Mutex.cpp Core/MultiThreading/ReaderWriterLock.cpp + Core/MultiThreading/Semaphore.cpp Core/MultiThreading/SharedMessageQueue.cpp Core/MultiThreading/ThreadedCommandProcessor.cpp Core/ImageFormats/ImageAccessor.cpp @@ -155,6 +156,16 @@ OrthancServer/ServerToolbox.cpp OrthancServer/OrthancFindRequestHandler.cpp OrthancServer/OrthancMoveRequestHandler.cpp + + # From "lua-scripting" branch + OrthancServer/DicomInstanceToStore.cpp + OrthancServer/Scheduler/DeleteInstanceCommand.cpp + OrthancServer/Scheduler/ModifyInstanceCommand.cpp + OrthancServer/Scheduler/ServerCommandInstance.cpp + OrthancServer/Scheduler/ServerJob.cpp + OrthancServer/Scheduler/ServerScheduler.cpp + OrthancServer/Scheduler/StorePeerCommand.cpp + OrthancServer/Scheduler/StoreScuCommand.cpp ) diff -r f4bbf13572cd -r 1fc112c4b832 Core/Lua/LuaContext.cpp --- a/Core/Lua/LuaContext.cpp Thu Jul 10 11:26:05 2014 +0200 +++ b/Core/Lua/LuaContext.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -34,6 +34,7 @@ #include "LuaContext.h" #include +#include extern "C" { @@ -78,7 +79,7 @@ lua_pop(state, 1); } - LOG(INFO) << "Lua says: " << result; + LOG(WARNING) << "Lua says: " << result; that->log_.append(result); that->log_.append("\n"); @@ -111,8 +112,6 @@ void LuaContext::Execute(std::string* output, const std::string& command) { - boost::mutex::scoped_lock lock(mutex_); - log_.clear(); int error = (luaL_loadbuffer(lua_, command.c_str(), command.size(), "line") || lua_pcall(lua_, 0, 0, 0)); @@ -143,7 +142,6 @@ bool LuaContext::IsExistingFunction(const char* name) { - boost::mutex::scoped_lock lock(mutex_); lua_settop(lua_, 0); lua_getglobal(lua_, name); return lua_type(lua_, -1) == LUA_TFUNCTION; diff -r f4bbf13572cd -r 1fc112c4b832 Core/Lua/LuaContext.h --- a/Core/Lua/LuaContext.h Thu Jul 10 11:26:05 2014 +0200 +++ b/Core/Lua/LuaContext.h Thu Jul 10 11:38:46 2014 +0200 @@ -34,8 +34,6 @@ #include "LuaException.h" -#include - extern "C" { #include @@ -43,6 +41,7 @@ #include +#include namespace Orthanc { @@ -52,7 +51,6 @@ friend class LuaFunctionCall; lua_State *lua_; - boost::mutex mutex_; std::string log_; static int PrintToLog(lua_State *L); diff -r f4bbf13572cd -r 1fc112c4b832 Core/Lua/LuaFunctionCall.cpp --- a/Core/Lua/LuaFunctionCall.cpp Thu Jul 10 11:26:05 2014 +0200 +++ b/Core/Lua/LuaFunctionCall.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -33,6 +33,10 @@ #include "../PrecompiledHeaders.h" #include "LuaFunctionCall.h" +#include +#include +#include +#include namespace Orthanc { @@ -47,7 +51,6 @@ LuaFunctionCall::LuaFunctionCall(LuaContext& context, const char* functionName) : context_(context), - lock_(context.mutex_), isExecuted_(false) { // Clear the stack to fulfill the invariant @@ -79,7 +82,7 @@ lua_pushnumber(context_.lua_, value); } - void LuaFunctionCall::PushJSON(const Json::Value& value) + void LuaFunctionCall::PushJson(const Json::Value& value) { CheckAlreadyExecuted(); @@ -118,7 +121,7 @@ lua_pushnumber(context_.lua_, i + 1); // Push the value of the cell - PushJSON(value[i]); + PushJson(value[i]); // Stores the pair in the table lua_rawset(context_.lua_, -3); @@ -137,7 +140,7 @@ lua_pushstring(context_.lua_, it->c_str()); // Push the value of the cell - PushJSON(value[*it]); + PushJson(value[*it]); // Stores the pair in the table lua_rawset(context_.lua_, -3); @@ -149,7 +152,7 @@ } } - void LuaFunctionCall::Execute(int numOutputs) + void LuaFunctionCall::ExecuteInternal(int numOutputs) { CheckAlreadyExecuted(); @@ -176,13 +179,8 @@ bool LuaFunctionCall::ExecutePredicate() { - Execute(1); - - if (lua_gettop(context_.lua_) == 0) - { - throw LuaException("No output was provided by the function"); - } - + ExecuteInternal(1); + if (!lua_isboolean(context_.lua_, 1)) { throw LuaException("The function is not a predicate (only true/false outputs allowed)"); @@ -190,4 +188,99 @@ return lua_toboolean(context_.lua_, 1) != 0; } + + + static void PopJson(Json::Value& result, + lua_State* lua, + int top) + { + if (lua_istable(lua, top)) + { + Json::Value tmp = Json::objectValue; + bool isArray = true; + size_t size = 0; + + // http://stackoverflow.com/a/6142700/881731 + + // Push another reference to the table on top of the stack (so we know + // where it is, and this function can work for negative, positive and + // pseudo indices + lua_pushvalue(lua, top); + // stack now contains: -1 => table + lua_pushnil(lua); + // stack now contains: -1 => nil; -2 => table + while (lua_next(lua, -2)) + { + // stack now contains: -1 => value; -2 => key; -3 => table + // copy the key so that lua_tostring does not modify the original + lua_pushvalue(lua, -2); + // stack now contains: -1 => key; -2 => value; -3 => key; -4 => table + std::string key(lua_tostring(lua, -1)); + Json::Value v; + PopJson(v, lua, -2); + + tmp[key] = v; + + size += 1; + try + { + if (boost::lexical_cast(key) != size) + { + isArray = false; + } + } + catch (boost::bad_lexical_cast&) + { + isArray = false; + } + + // pop value + copy of key, leaving original key + lua_pop(lua, 2); + // stack now contains: -1 => key; -2 => table + } + // stack now contains: -1 => table (when lua_next returns 0 it pops the key + // but does not push anything.) + // Pop table + lua_pop(lua, 1); + + // Stack is now the same as it was on entry to this function + + if (isArray) + { + result = Json::arrayValue; + for (size_t i = 0; i < size; i++) + { + result.append(tmp[boost::lexical_cast(i + 1)]); + } + } + else + { + result = tmp; + } + } + else if (lua_isnumber(lua, top)) + { + result = static_cast(lua_tonumber(lua, top)); + } + else if (lua_isstring(lua, top)) + { + result = std::string(lua_tostring(lua, top)); + } + else if (lua_isboolean(lua, top)) + { + result = static_cast(lua_toboolean(lua, top)); + } + else + { + LOG(WARNING) << "Unsupported Lua type when returning Json"; + result = Json::nullValue; + } + } + + + void LuaFunctionCall::ExecuteToJson(Json::Value& result) + { + ExecuteInternal(1); + PopJson(result, context_.lua_, lua_gettop(context_.lua_)); + } } diff -r f4bbf13572cd -r 1fc112c4b832 Core/Lua/LuaFunctionCall.h --- a/Core/Lua/LuaFunctionCall.h Thu Jul 10 11:26:05 2014 +0200 +++ b/Core/Lua/LuaFunctionCall.h Thu Jul 10 11:38:46 2014 +0200 @@ -36,18 +36,18 @@ #include - namespace Orthanc { class LuaFunctionCall : public boost::noncopyable { private: LuaContext& context_; - boost::mutex::scoped_lock lock_; bool isExecuted_; void CheckAlreadyExecuted(); + void ExecuteInternal(int numOutputs); + public: LuaFunctionCall(LuaContext& context, const char* functionName); @@ -60,10 +60,15 @@ void PushDouble(double value); - void PushJSON(const Json::Value& value); + void PushJson(const Json::Value& value); - void Execute(int numOutputs = 0); + void Execute() + { + ExecuteInternal(0); + } bool ExecutePredicate(); + + void ExecuteToJson(Json::Value& result); }; } diff -r f4bbf13572cd -r 1fc112c4b832 Core/MultiThreading/Semaphore.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/MultiThreading/Semaphore.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -0,0 +1,67 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "Semaphore.h" + +#include "../OrthancException.h" + + +namespace Orthanc +{ + Semaphore::Semaphore(unsigned int count) : count_(count) + { + if (count == 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + void Semaphore::Release() + { + boost::mutex::scoped_lock lock(mutex_); + + count_++; + condition_.notify_one(); + } + + void Semaphore::Acquire() + { + boost::mutex::scoped_lock lock(mutex_); + + while (count_ == 0) + { + condition_.wait(lock); + } + + count_++; + } +} diff -r f4bbf13572cd -r 1fc112c4b832 Core/MultiThreading/Semaphore.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/MultiThreading/Semaphore.h Thu Jul 10 11:38:46 2014 +0200 @@ -0,0 +1,54 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include +#include + +namespace Orthanc +{ + class Semaphore : public boost::noncopyable + { + private: + unsigned int count_; + boost::mutex mutex_; + boost::condition_variable condition_; + + public: + explicit Semaphore(unsigned int count); + + void Release(); + + void Acquire(); + }; +} diff -r f4bbf13572cd -r 1fc112c4b832 Core/MultiThreading/SharedMessageQueue.h --- a/Core/MultiThreading/SharedMessageQueue.h Thu Jul 10 11:26:05 2014 +0200 +++ b/Core/MultiThreading/SharedMessageQueue.h Thu Jul 10 11:38:46 2014 +0200 @@ -40,7 +40,7 @@ namespace Orthanc { - class SharedMessageQueue + class SharedMessageQueue : public boost::noncopyable { private: typedef std::list Queue; @@ -52,8 +52,8 @@ boost::condition_variable emptied_; public: - SharedMessageQueue(unsigned int maxSize = 0); - + explicit SharedMessageQueue(unsigned int maxSize = 0); + ~SharedMessageQueue(); // This transfers the ownership of the message diff -r f4bbf13572cd -r 1fc112c4b832 NEWS --- a/NEWS Thu Jul 10 11:26:05 2014 +0200 +++ b/NEWS Thu Jul 10 11:38:46 2014 +0200 @@ -4,6 +4,7 @@ Major changes ------------- +* Routing images with Lua scripts * Introduction of the Orthanc Plugin SDK * Official support of OS X (Darwin) diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/DatabaseWrapper.cpp --- a/OrthancServer/DatabaseWrapper.cpp Thu Jul 10 11:26:05 2014 +0200 +++ b/OrthancServer/DatabaseWrapper.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -1045,4 +1045,21 @@ result.push_back(s.ColumnInt64(0)); } } + + + void DatabaseWrapper::GetAllMetadata(std::map& result, + int64_t id) + { + result.clear(); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT type, value FROM Metadata WHERE id=?"); + s.BindInt64(0, id); + + while (s.Step()) + { + MetadataType key = static_cast(s.ColumnInt(0)); + result[key] = s.ColumnString(1); + } + } + } diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/DatabaseWrapper.h --- a/OrthancServer/DatabaseWrapper.h Thu Jul 10 11:26:05 2014 +0200 +++ b/OrthancServer/DatabaseWrapper.h Thu Jul 10 11:38:46 2014 +0200 @@ -235,5 +235,8 @@ void LookupTagValue(std::list& result, const std::string& value); + + void GetAllMetadata(std::map& result, + int64_t id); }; } diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/DicomInstanceToStore.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/DicomInstanceToStore.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -0,0 +1,174 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "DicomInstanceToStore.h" + +#include "FromDcmtkBridge.h" + +#include +#include + + +namespace Orthanc +{ + static DcmDataset& GetDataset(ParsedDicomFile& file) + { + return *reinterpret_cast(file.GetDcmtkObject())->getDataset(); + } + + + void DicomInstanceToStore::AddMetadata(ResourceType level, + MetadataType metadata, + const std::string& value) + { + metadata_[std::make_pair(level, metadata)] = value; + } + + + void DicomInstanceToStore::ComputeMissingInformation() + { + if (buffer_.HasContent() && + summary_.HasContent() && + json_.HasContent()) + { + // Fine, everything is available + return; + } + + if (!buffer_.HasContent()) + { + if (!parsed_.HasContent()) + { + throw OrthancException(ErrorCode_NotImplemented); + } + else + { + // Serialize the parsed DICOM file + buffer_.Allocate(); + if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer_.GetContent(), GetDataset(parsed_.GetContent()))) + { + LOG(ERROR) << "Unable to serialize a DICOM file to a memory buffer"; + throw OrthancException(ErrorCode_InternalError); + } + } + } + + if (summary_.HasContent() && + json_.HasContent()) + { + return; + } + + // At this point, we know that the DICOM file is available as a + // memory buffer, but that its summary or its JSON version is + // missing + + if (!parsed_.HasContent()) + { + parsed_.TakeOwnership(new ParsedDicomFile(buffer_.GetConstContent())); + } + + // At this point, we have parsed the DICOM file + + if (!summary_.HasContent()) + { + summary_.Allocate(); + FromDcmtkBridge::Convert(summary_.GetContent(), GetDataset(parsed_.GetContent())); + } + + if (!json_.HasContent()) + { + json_.Allocate(); + FromDcmtkBridge::ToJson(json_.GetContent(), GetDataset(parsed_.GetContent())); + } + } + + + + const char* DicomInstanceToStore::GetBufferData() + { + ComputeMissingInformation(); + + if (!buffer_.HasContent()) + { + throw OrthancException(ErrorCode_InternalError); + } + + if (buffer_.GetConstContent().size() == 0) + { + return NULL; + } + else + { + return buffer_.GetConstContent().c_str(); + } + } + + + size_t DicomInstanceToStore::GetBufferSize() + { + ComputeMissingInformation(); + + if (!buffer_.HasContent()) + { + throw OrthancException(ErrorCode_InternalError); + } + + return buffer_.GetConstContent().size(); + } + + + const DicomMap& DicomInstanceToStore::GetSummary() + { + ComputeMissingInformation(); + + if (!summary_.HasContent()) + { + throw OrthancException(ErrorCode_InternalError); + } + + return summary_.GetConstContent(); + } + + + const Json::Value& DicomInstanceToStore::GetJson() + { + ComputeMissingInformation(); + + if (!json_.HasContent()) + { + throw OrthancException(ErrorCode_InternalError); + } + + return json_.GetConstContent(); + } +} diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/DicomInstanceToStore.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/DicomInstanceToStore.h Thu Jul 10 11:38:46 2014 +0200 @@ -0,0 +1,204 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "ParsedDicomFile.h" +#include "ServerIndex.h" +#include "../Core/OrthancException.h" + +namespace Orthanc +{ + class DicomInstanceToStore + { + private: + template + class SmartContainer + { + private: + T* content_; + bool toDelete_; + bool isReadOnly_; + + void Deallocate() + { + if (content_ && toDelete_) + { + delete content_; + toDelete_ = false; + content_ = NULL; + } + } + + public: + SmartContainer() : content_(NULL), toDelete_(false) + { + } + + ~SmartContainer() + { + Deallocate(); + } + + void Allocate() + { + Deallocate(); + content_ = new T; + toDelete_ = true; + isReadOnly_ = false; + } + + void TakeOwnership(T* content) + { + if (content == NULL) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + Deallocate(); + content_ = content; + toDelete_ = true; + isReadOnly_ = false; + } + + void SetReference(T& content) // Read and write assign, without transfering ownership + { + Deallocate(); + content_ = &content; + toDelete_ = false; + isReadOnly_ = false; + } + + void SetConstReference(const T& content) // Read-only assign, without transfering ownership + { + Deallocate(); + content_ = &const_cast(content); + toDelete_ = false; + isReadOnly_ = true; + } + + bool HasContent() const + { + return content_ != NULL; + } + + T& GetContent() + { + if (content_ == NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + if (isReadOnly_) + { + throw OrthancException(ErrorCode_ReadOnly); + } + + return *content_; + } + + const T& GetConstContent() const + { + if (content_ == NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + return *content_; + } + }; + + + SmartContainer buffer_; + SmartContainer parsed_; + SmartContainer summary_; + SmartContainer json_; + + std::string remoteAet_; + ServerIndex::MetadataMap metadata_; + + void ComputeMissingInformation(); + + public: + void SetBuffer(const std::string& dicom) + { + buffer_.SetConstReference(dicom); + } + + void SetParsedDicomFile(ParsedDicomFile& parsed) + { + parsed_.SetReference(parsed); + } + + void SetSummary(const DicomMap& summary) + { + summary_.SetConstReference(summary); + } + + void SetJson(const Json::Value& json) + { + json_.SetConstReference(json); + } + + const std::string GetRemoteAet() const + { + return remoteAet_; + } + + void SetRemoteAet(const std::string& aet) + { + remoteAet_ = aet; + } + + void AddMetadata(ResourceType level, + MetadataType metadata, + const std::string& value); + + const ServerIndex::MetadataMap& GetMetadata() const + { + return metadata_; + } + + ServerIndex::MetadataMap& GetMetadata() + { + return metadata_; + } + + const char* GetBufferData(); + + size_t GetBufferSize(); + + const DicomMap& GetSummary(); + + const Json::Value& GetJson(); + }; +} diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/DicomProtocol/DicomUserConnection.cpp --- a/OrthancServer/DicomProtocol/DicomUserConnection.cpp Thu Jul 10 11:26:05 2014 +0200 +++ b/OrthancServer/DicomProtocol/DicomUserConnection.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -154,7 +154,8 @@ { if (cond.bad()) { - throw OrthancException("DicomUserConnection: " + std::string(cond.text())); + LOG(ERROR) << "DicomUserConnection: " << std::string(cond.text()); + throw OrthancException(ErrorCode_NetworkProtocol); } } @@ -162,7 +163,8 @@ { if (!IsOpen()) { - throw OrthancException("DicomUserConnection: First open the connection"); + LOG(ERROR) << "DicomUserConnection: First open the connection"; + throw OrthancException(ErrorCode_NetworkProtocol); } } diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/DicomProtocol/ReusableDicomUserConnection.cpp --- a/OrthancServer/DicomProtocol/ReusableDicomUserConnection.cpp Thu Jul 10 11:26:05 2014 +0200 +++ b/OrthancServer/DicomProtocol/ReusableDicomUserConnection.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -172,6 +172,14 @@ void ReusableDicomUserConnection::Unlock() { + if (connection_ != NULL && + connection_->GetDistantManufacturer() == ModalityManufacturer_StoreScp) + { + // "storescp" from DCMTK has problems when reusing a + // connection. Always close. + Close(); + } + lastUse_ = Now(); mutex_.unlock(); } diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/FromDcmtkBridge.cpp --- a/OrthancServer/FromDcmtkBridge.cpp Thu Jul 10 11:26:05 2014 +0200 +++ b/OrthancServer/FromDcmtkBridge.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -634,7 +634,7 @@ } bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer, - DcmDataset* dataSet) + DcmDataset& dataSet) { // Determine the transfer syntax which shall be used to write the // information to the file. We always switch to the Little Endian @@ -649,7 +649,7 @@ * dataset into memory. We now keep the original transfer syntax * (if available). **/ - E_TransferSyntax xfer = dataSet->getOriginalXfer(); + E_TransferSyntax xfer = dataSet.getOriginalXfer(); if (xfer == EXS_Unknown) { // No information about the original transfer syntax: This is @@ -660,7 +660,7 @@ E_EncodingType encodingType = /*opt_sequenceType*/ EET_ExplicitLength; // Create the meta-header information - DcmFileFormat ff(dataSet); + DcmFileFormat ff(&dataSet); ff.validateMetaInfo(xfer); // Create a memory buffer with the proper size diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/FromDcmtkBridge.h --- a/OrthancServer/FromDcmtkBridge.h Thu Jul 10 11:26:05 2014 +0200 +++ b/OrthancServer/FromDcmtkBridge.h Thu Jul 10 11:38:46 2014 +0200 @@ -104,6 +104,6 @@ static std::string GenerateUniqueIdentifier(ResourceType level); static bool SaveToMemoryBuffer(std::string& buffer, - DcmDataset* dataSet); + DcmDataset& dataSet); }; } diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/Internals/StoreScp.cpp --- a/OrthancServer/Internals/StoreScp.cpp Thu Jul 10 11:26:05 2014 +0200 +++ b/OrthancServer/Internals/StoreScp.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -168,7 +168,7 @@ FromDcmtkBridge::Convert(summary, **imageDataSet); FromDcmtkBridge::ToJson(dicomJson, **imageDataSet); - if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer, *imageDataSet)) + if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer, **imageDataSet)) { LOG(ERROR) << "cannot write DICOM file to memory"; rsp->DimseStatus = STATUS_STORE_Refused_OutOfResources; diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/OrthancInitialization.cpp --- a/OrthancServer/OrthancInitialization.cpp Thu Jul 10 11:26:05 2014 +0200 +++ b/OrthancServer/OrthancInitialization.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -288,7 +288,8 @@ if (modalities.type() != Json::objectValue || !modalities.isMember(name)) { - throw OrthancException(ErrorCode_BadFileFormat); + LOG(ERROR) << "No modality with symbolic name: " << name; + throw OrthancException(ErrorCode_InexistentItem); } try @@ -321,7 +322,8 @@ if (modalities.type() != Json::objectValue || !modalities.isMember(name)) { - throw OrthancException(ErrorCode_BadFileFormat); + LOG(ERROR) << "No peer with symbolic name: " << name; + throw OrthancException(ErrorCode_InexistentItem); } peer.FromJson(modalities[name]); diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Thu Jul 10 11:26:05 2014 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -33,7 +33,6 @@ #include "../PrecompiledHeadersServer.h" #include "OrthancRestApi.h" -#include "../DicomModification.h" #include "../FromDcmtkBridge.h" #include @@ -111,13 +110,11 @@ } - static bool ParseModifyRequest(DicomModification& target, - const RestApiPostCall& call) + + bool OrthancRestApi::ParseModifyRequest(DicomModification& target, + const Json::Value& request) { - // curl http://localhost:8042/series/95a6e2bf-9296e2cc-bf614e2f-22b391ee-16e010e0/modify -X POST -d '{"Replace":{"InstitutionName":"My own clinic"}}' - - Json::Value request; - if (call.ParseJsonRequest(request) && request.isObject()) + if (request.isObject()) { if (request.isMember("RemovePrivateTags")) { @@ -143,6 +140,23 @@ } + static bool ParseModifyRequest(DicomModification& target, + const RestApiPostCall& call) + { + // curl http://localhost:8042/series/95a6e2bf-9296e2cc-bf614e2f-22b391ee-16e010e0/modify -X POST -d '{"Replace":{"InstitutionName":"My own clinic"}}' + + Json::Value request; + if (call.ParseJsonRequest(request)) + { + return OrthancRestApi::ParseModifyRequest(target, request); + } + else + { + return false; + } + } + + static bool ParseAnonymizationRequest(DicomModification& target, RestApiPostCall& call) { @@ -252,47 +266,55 @@ /** - * Compute the resulting DICOM instance and store it into the Orthanc store. + * Compute the resulting DICOM instance. **/ std::auto_ptr modified(original.Clone()); modification.Apply(*modified); - std::string modifiedInstance; - if (context.Store(modifiedInstance, *modified) != StoreStatus_Success) - { - LOG(ERROR) << "Error while storing a modified instance " << *it; - return; - } + DicomInstanceToStore toStore; + toStore.SetParsedDicomFile(*modified); /** - * Record metadata information (AnonymizedFrom/ModifiedFrom). + * Prepare the metadata information to associate with the + * resulting DICOM instance (AnonymizedFrom/ModifiedFrom). **/ DicomInstanceHasher modifiedHasher = modified->GetHasher(); if (originalHasher.HashSeries() != modifiedHasher.HashSeries()) { - context.GetIndex().SetMetadata(modifiedHasher.HashSeries(), - metadataType, originalHasher.HashSeries()); + toStore.AddMetadata(ResourceType_Series, metadataType, originalHasher.HashSeries()); } if (originalHasher.HashStudy() != modifiedHasher.HashStudy()) { - context.GetIndex().SetMetadata(modifiedHasher.HashStudy(), - metadataType, originalHasher.HashStudy()); + toStore.AddMetadata(ResourceType_Study, metadataType, originalHasher.HashStudy()); } if (originalHasher.HashPatient() != modifiedHasher.HashPatient()) { - context.GetIndex().SetMetadata(modifiedHasher.HashPatient(), - metadataType, originalHasher.HashPatient()); + toStore.AddMetadata(ResourceType_Patient, metadataType, originalHasher.HashPatient()); } assert(*it == originalHasher.HashInstance()); + toStore.AddMetadata(ResourceType_Instance, metadataType, *it); + + + /** + * Store the resulting DICOM instance into the Orthanc store. + **/ + + std::string modifiedInstance; + if (context.Store(modifiedInstance, toStore) != StoreStatus_Success) + { + LOG(ERROR) << "Error while storing a modified instance " << *it; + return; + } + + // Sanity checks in debug mode assert(modifiedInstance == modifiedHasher.HashInstance()); - context.GetIndex().SetMetadata(modifiedInstance, metadataType, *it); /** @@ -423,8 +445,11 @@ modification.Apply(dicom); + DicomInstanceToStore toStore; + toStore.SetParsedDicomFile(dicom); + std::string id; - StoreStatus status = OrthancRestApi::GetContext(call).Store(id, dicom); + StoreStatus status = OrthancRestApi::GetContext(call).Store(id, toStore); if (status == StoreStatus_Failure) { diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/OrthancRestApi/OrthancRestApi.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestApi.cpp Thu Jul 10 11:26:05 2014 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestApi.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -71,8 +71,11 @@ LOG(INFO) << "Receiving a DICOM file of " << postData.size() << " bytes through HTTP"; + DicomInstanceToStore toStore; + toStore.SetBuffer(postData); + std::string publicId; - StoreStatus status = context.Store(publicId, postData); + StoreStatus status = context.Store(publicId, toStore); OrthancRestApi::GetApi(call).AnswerStoredInstance(call, publicId, status); } diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/OrthancRestApi/OrthancRestApi.h --- a/OrthancServer/OrthancRestApi/OrthancRestApi.h Thu Jul 10 11:26:05 2014 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestApi.h Thu Jul 10 11:38:46 2014 +0200 @@ -32,8 +32,9 @@ #pragma once +#include "../../Core/RestApi/RestApi.h" +#include "../DicomModification.h" #include "../ServerContext.h" -#include "../../Core/RestApi/RestApi.h" #include @@ -80,5 +81,8 @@ void AnswerStoredInstance(RestApiPostCall& call, const std::string& publicId, StoreStatus status) const; + + static bool ParseModifyRequest(DicomModification& target, + const Json::Value& request); }; } diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/OrthancRestApi/OrthancRestModalities.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Thu Jul 10 11:26:05 2014 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -36,6 +36,9 @@ #include "../OrthancInitialization.h" #include "../../Core/HttpClient.h" #include "../FromDcmtkBridge.h" +#include "../Scheduler/ServerJob.h" +#include "../Scheduler/StoreScuCommand.h" +#include "../Scheduler/StorePeerCommand.h" #include @@ -321,17 +324,16 @@ } RemoteModalityParameters p = Configuration::GetModalityUsingSymbolicName(remote); - ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), p); + ServerJob job; for (std::list::const_iterator it = instances.begin(); it != instances.end(); ++it) { - LOG(INFO) << "Sending resource " << *it << " to modality \"" << remote << "\""; + job.AddCommand(new StoreScuCommand(context, p)).AddInput(*it); + } - std::string dicom; - context.ReadFile(dicom, *it, FileContentType_Dicom); - locker.GetConnection().Store(dicom); - } + job.SetDescription("HTTP request: Store-SCU to peer \"" + remote + "\""); + context.GetScheduler().SubmitAndWait(job); call.GetOutput().AnswerBuffer("{}", "application/json"); } @@ -389,33 +391,15 @@ OrthancPeerParameters peer; Configuration::GetOrthancPeer(peer, remote); - // Configure the HTTP client - HttpClient client; - if (peer.GetUsername().size() != 0 && - peer.GetPassword().size() != 0) - { - client.SetCredentials(peer.GetUsername().c_str(), - peer.GetPassword().c_str()); - } - - client.SetUrl(peer.GetUrl() + "instances"); - client.SetMethod(HttpMethod_Post); - - // Loop over the instances that are to be sent + ServerJob job; for (std::list::const_iterator it = instances.begin(); it != instances.end(); ++it) { - LOG(INFO) << "Sending resource " << *it << " to peer \"" << remote << "\""; - - context.ReadFile(client.AccessPostData(), *it, FileContentType_Dicom); + job.AddCommand(new StorePeerCommand(context, peer)).AddInput(*it); + } - std::string answer; - if (!client.Apply(answer)) - { - LOG(ERROR) << "Unable to send resource " << *it << " to peer \"" << remote << "\""; - return; - } - } + job.SetDescription("HTTP request: POST to peer \"" + remote + "\""); + context.GetScheduler().SubmitAndWait(job); call.GetOutput().AnswerBuffer("{}", "application/json"); } diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/OrthancRestApi/OrthancRestSystem.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp Thu Jul 10 11:26:05 2014 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -90,7 +90,12 @@ { std::string result; ServerContext& context = OrthancRestApi::GetContext(call); - context.GetLuaContext().Execute(result, call.GetPostBody()); + + { + ServerContext::LuaContextLocker locker(context); + locker.GetLua().Execute(result, call.GetPostBody()); + } + call.GetOutput().AnswerBuffer(result, "text/plain"); } diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/ParsedDicomFile.cpp --- a/OrthancServer/ParsedDicomFile.cpp Thu Jul 10 11:26:05 2014 +0200 +++ b/OrthancServer/ParsedDicomFile.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -873,7 +873,7 @@ void ParsedDicomFile::Answer(RestApiOutput& output) { std::string serialized; - if (FromDcmtkBridge::SaveToMemoryBuffer(serialized, pimpl_->file_->getDataset())) + if (FromDcmtkBridge::SaveToMemoryBuffer(serialized, *pimpl_->file_->getDataset())) { output.AnswerBuffer(serialized, CONTENT_TYPE_OCTET_STREAM); } @@ -956,7 +956,7 @@ void ParsedDicomFile::SaveToMemoryBuffer(std::string& buffer) { - FromDcmtkBridge::SaveToMemoryBuffer(buffer, pimpl_->file_->getDataset()); + FromDcmtkBridge::SaveToMemoryBuffer(buffer, *pimpl_->file_->getDataset()); } diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/Scheduler/DeleteInstanceCommand.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/DeleteInstanceCommand.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -0,0 +1,53 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "DeleteInstanceCommand.h" + +#include + +namespace Orthanc +{ + bool DeleteInstanceCommand::Apply(ListOfStrings& outputs, + const ListOfStrings& inputs) + { + for (ListOfStrings::const_iterator + it = inputs.begin(); it != inputs.end(); ++it) + { + LOG(INFO) << "Deleting instance " << *it; + + Json::Value tmp; + context_.GetIndex().DeleteResource(tmp, *it, ResourceType_Instance); + } + + return true; + } +} diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/Scheduler/DeleteInstanceCommand.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/DeleteInstanceCommand.h Thu Jul 10 11:38:46 2014 +0200 @@ -0,0 +1,53 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IServerCommand.h" +#include "../ServerContext.h" + +namespace Orthanc +{ + class DeleteInstanceCommand : public IServerCommand + { + private: + ServerContext& context_; + + public: + DeleteInstanceCommand(ServerContext& context) : context_(context) + { + } + + virtual bool Apply(ListOfStrings& outputs, + const ListOfStrings& inputs); + }; +} diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/Scheduler/IServerCommand.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/IServerCommand.h Thu Jul 10 11:38:46 2014 +0200 @@ -0,0 +1,53 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include +#include +#include + +namespace Orthanc +{ + class IServerCommand : public boost::noncopyable + { + public: + typedef std::list ListOfStrings; + + virtual ~IServerCommand() + { + } + + virtual bool Apply(ListOfStrings& outputs, + const ListOfStrings& inputs) = 0; + }; +} diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/Scheduler/ModifyInstanceCommand.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/ModifyInstanceCommand.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -0,0 +1,69 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "ModifyInstanceCommand.h" + +#include + +namespace Orthanc +{ + bool ModifyInstanceCommand::Apply(ListOfStrings& outputs, + const ListOfStrings& inputs) + { + for (ListOfStrings::const_iterator + it = inputs.begin(); it != inputs.end(); ++it) + { + LOG(INFO) << "Modifying resource " << *it; + + std::auto_ptr modified; + + { + ServerContext::DicomCacheLocker lock(context_, *it); + modified.reset(lock.GetDicom().Clone()); + } + + modification_.Apply(*modified); + + DicomInstanceToStore toStore; + toStore.SetParsedDicomFile(*modified); + // TODO other metadata + toStore.AddMetadata(ResourceType_Instance, MetadataType_ModifiedFrom, *it); + + std::string modifiedId; + context_.Store(modifiedId, toStore); + + outputs.push_back(modifiedId); + } + + return true; + } +} diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/Scheduler/ModifyInstanceCommand.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/ModifyInstanceCommand.h Thu Jul 10 11:38:46 2014 +0200 @@ -0,0 +1,66 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IServerCommand.h" +#include "../ServerContext.h" +#include "../DicomModification.h" + +namespace Orthanc +{ + class ModifyInstanceCommand : public IServerCommand + { + private: + ServerContext& context_; + DicomModification modification_; + + public: + ModifyInstanceCommand(ServerContext& context) : + context_(context) + { + } + + DicomModification& GetModification() + { + return modification_; + } + + const DicomModification& GetModification() const + { + return modification_; + } + + virtual bool Apply(ListOfStrings& outputs, + const ListOfStrings& inputs); + }; +} diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/Scheduler/ServerCommandInstance.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/ServerCommandInstance.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -0,0 +1,97 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "ServerCommandInstance.h" + +#include "../../Core/OrthancException.h" + +namespace Orthanc +{ + bool ServerCommandInstance::Execute(IListener& listener) + { + ListOfStrings outputs; + + bool success = false; + + try + { + if (command_->Apply(outputs, inputs_)) + { + success = true; + } + } + catch (OrthancException&) + { + } + + if (!success) + { + listener.SignalFailure(jobId_); + return true; + } + + for (std::list::iterator + it = next_.begin(); it != next_.end(); it++) + { + for (ListOfStrings::const_iterator + output = outputs.begin(); output != outputs.end(); output++) + { + (*it)->AddInput(*output); + } + } + + listener.SignalSuccess(jobId_); + return true; + } + + + ServerCommandInstance::ServerCommandInstance(IServerCommand *command, + const std::string& jobId) : + command_(command), + jobId_(jobId), + connectedToSink_(false) + { + if (command_ == NULL) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + ServerCommandInstance::~ServerCommandInstance() + { + if (command_ != NULL) + { + delete command_; + } + } +} diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/Scheduler/ServerCommandInstance.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/ServerCommandInstance.h Thu Jul 10 11:38:46 2014 +0200 @@ -0,0 +1,104 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../../Core/IDynamicObject.h" +#include "IServerCommand.h" + +namespace Orthanc +{ + class ServerCommandInstance : public IDynamicObject + { + friend class ServerScheduler; + + public: + class IListener + { + public: + virtual ~IListener() + { + } + + virtual void SignalSuccess(const std::string& jobId) = 0; + + virtual void SignalFailure(const std::string& jobId) = 0; + }; + + private: + typedef IServerCommand::ListOfStrings ListOfStrings; + + IServerCommand *command_; + std::string jobId_; + ListOfStrings inputs_; + std::list next_; + bool connectedToSink_; + + bool Execute(IListener& listener); + + public: + ServerCommandInstance(IServerCommand *command, + const std::string& jobId); + + virtual ~ServerCommandInstance(); + + const std::string& GetJobId() const + { + return jobId_; + } + + void AddInput(const std::string& input) + { + inputs_.push_back(input); + } + + void ConnectOutput(ServerCommandInstance& next) + { + next_.push_back(&next); + } + + void SetConnectedToSink(bool connected = true) + { + connectedToSink_ = connected; + } + + bool IsConnectedToSink() const + { + return connectedToSink_; + } + + const std::list& GetNextCommands() const + { + return next_; + } + }; +} diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/Scheduler/ServerJob.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/ServerJob.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -0,0 +1,146 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "ServerJob.h" + +#include "../../Core/OrthancException.h" +#include "../../Core/Toolbox.h" +#include "../../Core/Uuid.h" + +namespace Orthanc +{ + void ServerJob::CheckOrdering() + { + std::map index; + + unsigned int count = 0; + for (std::list::const_iterator + it = filters_.begin(); it != filters_.end(); it++) + { + index[*it] = count++; + } + + for (std::list::const_iterator + it = filters_.begin(); it != filters_.end(); it++) + { + const std::list& nextCommands = (*it)->GetNextCommands(); + + for (std::list::const_iterator + next = nextCommands.begin(); next != nextCommands.end(); next++) + { + if (index.find(*next) == index.end() || + index[*next] <= index[*it]) + { + // You must reorder your calls to "ServerJob::AddCommand" + throw OrthancException("Bad ordering of filters in a job"); + } + } + } + } + + + size_t ServerJob::Submit(SharedMessageQueue& target, + ServerCommandInstance::IListener& listener) + { + if (submitted_) + { + // This job has already been submitted + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + CheckOrdering(); + + size_t size = filters_.size(); + + for (std::list::iterator + it = filters_.begin(); it != filters_.end(); it++) + { + target.Enqueue(*it); + } + + filters_.clear(); + submitted_ = true; + + return size; + } + + + ServerJob::ServerJob() + { + jobId_ = Toolbox::GenerateUuid(); + submitted_ = false; + description_ = "no description"; + } + + + ServerJob::~ServerJob() + { + for (std::list::iterator + it = filters_.begin(); it != filters_.end(); it++) + { + delete *it; + } + + for (std::list::iterator + it = payloads_.begin(); it != payloads_.end(); it++) + { + delete *it; + } + } + + + ServerCommandInstance& ServerJob::AddCommand(IServerCommand* filter) + { + if (submitted_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + filters_.push_back(new ServerCommandInstance(filter, jobId_)); + + return *filters_.back(); + } + + + IDynamicObject& ServerJob::AddPayload(IDynamicObject* payload) + { + if (submitted_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + payloads_.push_back(payload); + + return *filters_.back(); + } + +} diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/Scheduler/ServerJob.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/ServerJob.h Thu Jul 10 11:38:46 2014 +0200 @@ -0,0 +1,82 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "ServerCommandInstance.h" +#include "../../Core/MultiThreading/SharedMessageQueue.h" + +namespace Orthanc +{ + class ServerJob + { + friend class ServerScheduler; + + private: + std::list filters_; + std::list payloads_; + std::string jobId_; + bool submitted_; + std::string description_; + + void CheckOrdering(); + + size_t Submit(SharedMessageQueue& target, + ServerCommandInstance::IListener& listener); + + public: + ServerJob(); + + ~ServerJob(); + + const std::string& GetId() const + { + return jobId_; + } + + void SetDescription(const std::string& description) + { + description_ = description; + } + + const std::string& GetDescription() const + { + return description_; + } + + ServerCommandInstance& AddCommand(IServerCommand* filter); + + // Take the ownership of a payload to a job. This payload will be + // automatically freed when the job succeeds or fails. + IDynamicObject& AddPayload(IDynamicObject* payload); + }; +} diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/Scheduler/ServerScheduler.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/ServerScheduler.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -0,0 +1,336 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "ServerScheduler.h" + +#include "../../Core/OrthancException.h" + +#include + +namespace Orthanc +{ + namespace + { + // Anonymous namespace to avoid clashes between compilation modules + class Sink : public IServerCommand + { + private: + ListOfStrings& target_; + + public: + Sink(ListOfStrings& target) : target_(target) + { + } + + virtual bool Apply(ListOfStrings& outputs, + const ListOfStrings& inputs) + { + for (ListOfStrings::const_iterator + it = inputs.begin(); it != inputs.end(); it++) + { + target_.push_back(*it); + } + + return true; + } + }; + } + + + ServerScheduler::JobInfo& ServerScheduler::GetJobInfo(const std::string& jobId) + { + Jobs::iterator info = jobs_.find(jobId); + + if (info == jobs_.end()) + { + throw OrthancException(ErrorCode_InternalError); + } + + return info->second; + } + + + void ServerScheduler::SignalSuccess(const std::string& jobId) + { + boost::mutex::scoped_lock lock(mutex_); + + JobInfo& info = GetJobInfo(jobId); + info.success_++; + + assert(info.failures_ == 0); + + if (info.success_ >= info.size_) + { + if (info.watched_) + { + watchedJobStatus_[jobId] = JobStatus_Success; + watchedJobFinished_.notify_all(); + } + + LOG(INFO) << "Job successfully finished (" << info.description_ << ")"; + jobs_.erase(jobId); + + availableJob_.Release(); + } + } + + + void ServerScheduler::SignalFailure(const std::string& jobId) + { + boost::mutex::scoped_lock lock(mutex_); + + JobInfo& info = GetJobInfo(jobId); + info.failures_++; + + if (info.success_ + info.failures_ >= info.size_) + { + if (info.watched_) + { + watchedJobStatus_[jobId] = JobStatus_Failure; + watchedJobFinished_.notify_all(); + } + + LOG(ERROR) << "Job has failed (" << info.description_ << ")"; + jobs_.erase(jobId); + + availableJob_.Release(); + } + } + + + void ServerScheduler::Worker(ServerScheduler* that) + { + static const int32_t TIMEOUT = 100; + + LOG(WARNING) << "The server scheduler has started"; + + while (!that->finish_) + { + std::auto_ptr object(that->queue_.Dequeue(TIMEOUT)); + if (object.get() != NULL) + { + ServerCommandInstance& filter = dynamic_cast(*object); + + // Skip the execution of this filter if its parent job has + // previously failed. + bool jobHasFailed; + { + boost::mutex::scoped_lock lock(that->mutex_); + JobInfo& info = that->GetJobInfo(filter.GetJobId()); + jobHasFailed = (info.failures_ > 0 || info.cancel_); + } + + if (jobHasFailed) + { + that->SignalFailure(filter.GetJobId()); + } + else + { + filter.Execute(*that); + } + } + } + } + + + void ServerScheduler::SubmitInternal(ServerJob& job, + bool watched) + { + availableJob_.Acquire(); + + boost::mutex::scoped_lock lock(mutex_); + + JobInfo info; + info.size_ = job.Submit(queue_, *this); + info.cancel_ = false; + info.success_ = 0; + info.failures_ = 0; + info.description_ = job.GetDescription(); + info.watched_ = watched; + + assert(info.size_ > 0); + + if (watched) + { + watchedJobStatus_[job.GetId()] = JobStatus_Running; + } + + jobs_[job.GetId()] = info; + + LOG(INFO) << "New job submitted (" << job.description_ << ")"; + } + + + ServerScheduler::ServerScheduler(unsigned int maxJobs) : availableJob_(maxJobs) + { + finish_ = false; + worker_ = boost::thread(Worker, this); + } + + + ServerScheduler::~ServerScheduler() + { + finish_ = true; + worker_.join(); + } + + + void ServerScheduler::Submit(ServerJob& job) + { + if (job.filters_.empty()) + { + return; + } + + SubmitInternal(job, false); + } + + + bool ServerScheduler::SubmitAndWait(ListOfStrings& outputs, + ServerJob& job) + { + std::string jobId = job.GetId(); + + outputs.clear(); + + if (job.filters_.empty()) + { + return true; + } + + // Add a sink filter to collect all the results of the filters + // that have no next filter. + ServerCommandInstance& sink = job.AddCommand(new Sink(outputs)); + + for (std::list::iterator + it = job.filters_.begin(); it != job.filters_.end(); it++) + { + if ((*it) != &sink && + (*it)->IsConnectedToSink()) + { + (*it)->ConnectOutput(sink); + } + } + + // Submit the job + SubmitInternal(job, true); + + // Wait for the job to complete (either success or failure) + JobStatus status; + + { + boost::mutex::scoped_lock lock(mutex_); + + assert(watchedJobStatus_.find(jobId) != watchedJobStatus_.end()); + + while (watchedJobStatus_[jobId] == JobStatus_Running) + { + watchedJobFinished_.wait(lock); + } + + status = watchedJobStatus_[jobId]; + watchedJobStatus_.erase(jobId); + } + + return (status == JobStatus_Success); + } + + + bool ServerScheduler::SubmitAndWait(ServerJob& job) + { + ListOfStrings ignoredSink; + return SubmitAndWait(ignoredSink, job); + } + + + bool ServerScheduler::IsRunning(const std::string& jobId) + { + boost::mutex::scoped_lock lock(mutex_); + return jobs_.find(jobId) != jobs_.end(); + } + + + void ServerScheduler::Cancel(const std::string& jobId) + { + boost::mutex::scoped_lock lock(mutex_); + + Jobs::iterator job = jobs_.find(jobId); + + if (job != jobs_.end()) + { + job->second.cancel_ = true; + LOG(WARNING) << "Canceling a job (" << job->second.description_ << ")"; + } + } + + + float ServerScheduler::GetProgress(const std::string& jobId) + { + boost::mutex::scoped_lock lock(mutex_); + + Jobs::iterator job = jobs_.find(jobId); + + if (job == jobs_.end() || + job->second.size_ == 0 /* should never happen */) + { + // This job is not running + return 1; + } + + if (job->second.failures_ != 0) + { + return 1; + } + + if (job->second.size_ == 1) + { + return job->second.success_; + } + + return (static_cast(job->second.success_) / + static_cast(job->second.size_ - 1)); + } + + + void ServerScheduler::GetListOfJobs(ListOfStrings& jobs) + { + boost::mutex::scoped_lock lock(mutex_); + + jobs.clear(); + + for (Jobs::const_iterator + it = jobs_.begin(); it != jobs_.end(); it++) + { + jobs.push_back(it->first); + } + } +} diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/Scheduler/ServerScheduler.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/ServerScheduler.h Thu Jul 10 11:38:46 2014 +0200 @@ -0,0 +1,120 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "ServerJob.h" + +#include "../../Core/MultiThreading/Semaphore.h" + +namespace Orthanc +{ + class ServerScheduler : public ServerCommandInstance::IListener + { + private: + struct JobInfo + { + bool watched_; + bool cancel_; + size_t size_; + size_t success_; + size_t failures_; + std::string description_; + }; + + enum JobStatus + { + JobStatus_Running = 1, + JobStatus_Success = 2, + JobStatus_Failure = 3 + }; + + typedef IServerCommand::ListOfStrings ListOfStrings; + typedef std::map Jobs; + + boost::mutex mutex_; + boost::condition_variable watchedJobFinished_; + Jobs jobs_; + SharedMessageQueue queue_; + bool finish_; + boost::thread worker_; + std::map watchedJobStatus_; + Semaphore availableJob_; + + JobInfo& GetJobInfo(const std::string& jobId); + + virtual void SignalSuccess(const std::string& jobId); + + virtual void SignalFailure(const std::string& jobId); + + static void Worker(ServerScheduler* that); + + void SubmitInternal(ServerJob& job, + bool watched); + + public: + ServerScheduler(unsigned int maxjobs); + + ~ServerScheduler(); + + void Submit(ServerJob& job); + + bool SubmitAndWait(ListOfStrings& outputs, + ServerJob& job); + + bool SubmitAndWait(ServerJob& job); + + bool IsRunning(const std::string& jobId); + + void Cancel(const std::string& jobId); + + // Returns a number between 0 and 1 + float GetProgress(const std::string& jobId); + + bool IsRunning(const ServerJob& job) + { + return IsRunning(job.GetId()); + } + + void Cancel(const ServerJob& job) + { + Cancel(job.GetId()); + } + + float GetProgress(const ServerJob& job) + { + return GetProgress(job.GetId()); + } + + void GetListOfJobs(ListOfStrings& jobs); + }; +} diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/Scheduler/StorePeerCommand.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/StorePeerCommand.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -0,0 +1,83 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "StorePeerCommand.h" + +#include "../../Core/HttpClient.h" + +#include + +namespace Orthanc +{ + StorePeerCommand::StorePeerCommand(ServerContext& context, + const OrthancPeerParameters& peer) : + context_(context), + peer_(peer) + { + } + + bool StorePeerCommand::Apply(ListOfStrings& outputs, + const ListOfStrings& inputs) + { + // Configure the HTTP client + HttpClient client; + if (peer_.GetUsername().size() != 0 && + peer_.GetPassword().size() != 0) + { + client.SetCredentials(peer_.GetUsername().c_str(), + peer_.GetPassword().c_str()); + } + + client.SetUrl(peer_.GetUrl() + "instances"); + client.SetMethod(HttpMethod_Post); + + for (ListOfStrings::const_iterator + it = inputs.begin(); it != inputs.end(); ++it) + { + LOG(INFO) << "Sending resource " << *it << " to peer \"" + << peer_.GetUrl() << "\""; + + context_.ReadFile(client.AccessPostData(), *it, FileContentType_Dicom); + + std::string answer; + if (!client.Apply(answer)) + { + LOG(ERROR) << "Unable to send resource " << *it << " to peer \"" << peer_.GetUrl() << "\""; + throw OrthancException(ErrorCode_NetworkProtocol); + } + + outputs.push_back(*it); + } + + return true; + } +} diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/Scheduler/StorePeerCommand.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/StorePeerCommand.h Thu Jul 10 11:38:46 2014 +0200 @@ -0,0 +1,54 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IServerCommand.h" +#include "../ServerContext.h" +#include "../OrthancInitialization.h" + +namespace Orthanc +{ + class StorePeerCommand : public IServerCommand + { + private: + ServerContext& context_; + OrthancPeerParameters peer_; + + public: + StorePeerCommand(ServerContext& context, + const OrthancPeerParameters& peer); + + virtual bool Apply(ListOfStrings& outputs, + const ListOfStrings& inputs); + }; +} diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/Scheduler/StoreScuCommand.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/StoreScuCommand.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -0,0 +1,66 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "StoreScuCommand.h" + +#include + +namespace Orthanc +{ + StoreScuCommand::StoreScuCommand(ServerContext& context, + const RemoteModalityParameters& modality) : + context_(context), + modality_(modality) + { + } + + bool StoreScuCommand::Apply(ListOfStrings& outputs, + const ListOfStrings& inputs) + { + ReusableDicomUserConnection::Locker locker(context_.GetReusableDicomUserConnection(), modality_); + + for (ListOfStrings::const_iterator + it = inputs.begin(); it != inputs.end(); ++it) + { + LOG(INFO) << "Sending resource " << *it << " to modality \"" + << modality_.GetApplicationEntityTitle() << "\""; + + std::string dicom; + context_.ReadFile(dicom, *it, FileContentType_Dicom); + locker.GetConnection().Store(dicom); + + outputs.push_back(*it); + } + + return true; + } +} diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/Scheduler/StoreScuCommand.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/StoreScuCommand.h Thu Jul 10 11:38:46 2014 +0200 @@ -0,0 +1,53 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, + * Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IServerCommand.h" +#include "../ServerContext.h" + +namespace Orthanc +{ + class StoreScuCommand : public IServerCommand + { + private: + ServerContext& context_; + RemoteModalityParameters modality_; + + public: + StoreScuCommand(ServerContext& context, + const RemoteModalityParameters& modality); + + virtual bool Apply(ListOfStrings& outputs, + const ListOfStrings& inputs); + }; +} diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/ServerContext.cpp --- a/OrthancServer/ServerContext.cpp Thu Jul 10 11:26:05 2014 +0200 +++ b/OrthancServer/ServerContext.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -43,9 +43,19 @@ #include #include + +#include "Scheduler/DeleteInstanceCommand.h" +#include "Scheduler/ModifyInstanceCommand.h" +#include "Scheduler/StoreScuCommand.h" +#include "Scheduler/StorePeerCommand.h" +#include "OrthancRestApi/OrthancRestApi.h" + + + #define ENABLE_DICOM_CACHE 1 static const char* RECEIVED_INSTANCE_FILTER = "ReceivedInstanceFilter"; +static const char* ON_STORED_INSTANCE = "OnStoredInstance"; static const size_t DICOM_CACHE_SIZE = 2; @@ -67,7 +77,8 @@ accessor_(storage_), compressionEnabled_(false), provider_(*this), - dicomCache_(provider_, DICOM_CACHE_SIZE) + dicomCache_(provider_, DICOM_CACHE_SIZE), + scheduler_(Configuration::GetGlobalIntegerParameter("LimitJobs", 10)) { scu_.SetLocalApplicationEntityTitle(Configuration::GetGlobalStringParameter("DicomAet", "ORTHANC")); //scu_.SetMillisecondsBeforeClose(1); // The connection is always released @@ -90,76 +101,233 @@ storage_.Remove(fileUuid); } - StoreStatus ServerContext::Store(const char* dicomInstance, - size_t dicomSize, - const DicomMap& dicomSummary, - const Json::Value& dicomJson, - const std::string& remoteAet) + + bool ServerContext::ApplyReceivedInstanceFilter(const Json::Value& simplified, + const std::string& remoteAet) { - // Test if the instance must be filtered out - if (lua_.IsExistingFunction(RECEIVED_INSTANCE_FILTER)) + LuaContextLocker locker(*this); + + if (locker.GetLua().IsExistingFunction(RECEIVED_INSTANCE_FILTER)) { - Json::Value simplified; - SimplifyTags(simplified, dicomJson); - - LuaFunctionCall call(lua_, RECEIVED_INSTANCE_FILTER); - call.PushJSON(simplified); + LuaFunctionCall call(locker.GetLua(), RECEIVED_INSTANCE_FILTER); + call.PushJson(simplified); call.PushString(remoteAet); if (!call.ExecutePredicate()) { + return false; + } + } + + return true; + } + + + static IServerCommand* ParseOperation(ServerContext& context, + const std::string& operation, + const Json::Value& parameters) + { + if (operation == "delete") + { + LOG(INFO) << "Lua script to delete instance " << parameters["Instance"].asString(); + return new DeleteInstanceCommand(context); + } + + if (operation == "store-scu") + { + std::string modality = parameters["Modality"].asString(); + LOG(INFO) << "Lua script to send instance " << parameters["Instance"].asString() + << " to modality " << modality << " using Store-SCU"; + return new StoreScuCommand(context, Configuration::GetModalityUsingSymbolicName(modality)); + } + + if (operation == "store-peer") + { + std::string peer = parameters["Peer"].asString(); + LOG(INFO) << "Lua script to send instance " << parameters["Instance"].asString() + << " to peer " << peer << " using HTTP"; + + OrthancPeerParameters parameters; + Configuration::GetOrthancPeer(parameters, peer); + return new StorePeerCommand(context, parameters); + } + + if (operation == "modify") + { + LOG(INFO) << "Lua script to modify instance " << parameters["Instance"].asString(); + std::auto_ptr command(new ModifyInstanceCommand(context)); + OrthancRestApi::ParseModifyRequest(command->GetModification(), parameters); + return command.release(); + } + + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + + void ServerContext::ApplyOnStoredInstance(const std::string& instanceId, + const Json::Value& simplifiedDicom, + const Json::Value& metadata) + { + LuaContextLocker locker(*this); + + if (locker.GetLua().IsExistingFunction(ON_STORED_INSTANCE)) + { + locker.GetLua().Execute("_InitializeJob()"); + + LuaFunctionCall call(locker.GetLua(), ON_STORED_INSTANCE); + call.PushString(instanceId); + call.PushJson(simplifiedDicom); + call.PushJson(metadata); + call.Execute(); + + Json::Value operations; + LuaFunctionCall call2(locker.GetLua(), "_AccessJob"); + call2.ExecuteToJson(operations); + + if (operations.type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_InternalError); + } + + ServerJob job; + ServerCommandInstance* previousCommand = NULL; + + for (Json::Value::ArrayIndex i = 0; i < operations.size(); ++i) + { + if (operations[i].type() != Json::objectValue || + !operations[i].isMember("Operation")) + { + throw OrthancException(ErrorCode_InternalError); + } + + const Json::Value& parameters = operations[i]; + std::string operation = parameters["Operation"].asString(); + + ServerCommandInstance& command = job.AddCommand(ParseOperation(*this, operation, operations[i])); + + if (!parameters.isMember("Instance")) + { + throw OrthancException(ErrorCode_InternalError); + } + + std::string instance = parameters["Instance"].asString(); + if (instance.empty()) + { + previousCommand->ConnectOutput(command); + } + else + { + command.AddInput(instance); + } + + previousCommand = &command; + } + + job.SetDescription(std::string("Lua script: ") + ON_STORED_INSTANCE); + scheduler_.Submit(job); + } + } + + + StoreStatus ServerContext::Store(std::string& resultPublicId, + DicomInstanceToStore& dicom) + { + try + { + DicomInstanceHasher hasher(dicom.GetSummary()); + resultPublicId = hasher.HashInstance(); + + Json::Value simplified; + SimplifyTags(simplified, dicom.GetJson()); + + // Test if the instance must be filtered out + if (!ApplyReceivedInstanceFilter(simplified, dicom.GetRemoteAet())) + { LOG(INFO) << "An incoming instance has been discarded by the filter"; return StoreStatus_FilteredOut; } - } + + if (compressionEnabled_) + { + accessor_.SetCompressionForNextOperations(CompressionType_Zlib); + } + else + { + accessor_.SetCompressionForNextOperations(CompressionType_None); + } + + FileInfo dicomInfo = accessor_.Write(dicom.GetBufferData(), dicom.GetBufferSize(), FileContentType_Dicom); + FileInfo jsonInfo = accessor_.Write(dicom.GetJson().toStyledString(), FileContentType_DicomAsJson); - if (compressionEnabled_) - { - accessor_.SetCompressionForNextOperations(CompressionType_Zlib); - } - else - { - accessor_.SetCompressionForNextOperations(CompressionType_None); - } + ServerIndex::Attachments attachments; + attachments.push_back(dicomInfo); + attachments.push_back(jsonInfo); + + std::map instanceMetadata; + StoreStatus status = index_.Store(instanceMetadata, dicom.GetSummary(), attachments, + dicom.GetRemoteAet(), dicom.GetMetadata()); + + if (status != StoreStatus_Success) + { + storage_.Remove(dicomInfo.GetUuid()); + storage_.Remove(jsonInfo.GetUuid()); + } + + switch (status) + { + case StoreStatus_Success: + LOG(INFO) << "New instance stored"; + break; - FileInfo dicomInfo = accessor_.Write(dicomInstance, dicomSize, FileContentType_Dicom); - FileInfo jsonInfo = accessor_.Write(dicomJson.toStyledString(), FileContentType_DicomAsJson); + case StoreStatus_AlreadyStored: + LOG(INFO) << "Already stored"; + break; + + case StoreStatus_Failure: + LOG(ERROR) << "Store failure"; + break; + + default: + // This should never happen + break; + } - ServerIndex::Attachments attachments; - attachments.push_back(dicomInfo); - attachments.push_back(jsonInfo); + if (status == StoreStatus_Success || + status == StoreStatus_AlreadyStored) + { + Json::Value metadata = Json::objectValue; + for (std::map::const_iterator + it = instanceMetadata.begin(); + it != instanceMetadata.end(); ++it) + { + metadata[EnumerationToString(it->first)] = it->second; + } - StoreStatus status = index_.Store(dicomSummary, attachments, remoteAet); + try + { + ApplyOnStoredInstance(resultPublicId, simplified, metadata); + } + catch (OrthancException& e) + { + LOG(ERROR) << "Error in OnStoredInstance callback (Lua): " << e.What(); + } + } - if (status != StoreStatus_Success) + return status; + } + catch (OrthancException& e) { - storage_.Remove(dicomInfo.GetUuid()); - storage_.Remove(jsonInfo.GetUuid()); - } - - switch (status) - { - case StoreStatus_Success: - LOG(INFO) << "New instance stored"; - break; + if (e.GetErrorCode() == ErrorCode_InexistentTag) + { + LogMissingRequiredTag(dicom.GetSummary()); + } - case StoreStatus_AlreadyStored: - LOG(INFO) << "Already stored"; - break; - - case StoreStatus_Failure: - LOG(ERROR) << "Store failure"; - break; - - default: - // This should never happen - break; + throw; } - - return status; } - + + void ServerContext::AnswerDicomFile(RestApiOutput& output, const std::string& instancePublicId, FileContentType content) @@ -249,87 +417,6 @@ } - static DcmFileFormat& GetDicom(ParsedDicomFile& file) - { - return *reinterpret_cast(file.GetDcmtkObject()); - } - - - StoreStatus ServerContext::Store(std::string& resultPublicId, - ParsedDicomFile& dicomInstance, - const char* dicomBuffer, - size_t dicomSize) - { - DicomMap dicomSummary; - FromDcmtkBridge::Convert(dicomSummary, *GetDicom(dicomInstance).getDataset()); - - try - { - DicomInstanceHasher hasher(dicomSummary); - resultPublicId = hasher.HashInstance(); - - Json::Value dicomJson; - FromDcmtkBridge::ToJson(dicomJson, *GetDicom(dicomInstance).getDataset()); - - StoreStatus status = StoreStatus_Failure; - if (dicomSize > 0) - { - status = Store(dicomBuffer, dicomSize, dicomSummary, dicomJson, ""); - } - - return status; - } - catch (OrthancException& e) - { - if (e.GetErrorCode() == ErrorCode_InexistentTag) - { - LogMissingRequiredTag(dicomSummary); - } - - throw; - } - } - - - StoreStatus ServerContext::Store(std::string& resultPublicId, - ParsedDicomFile& dicomInstance) - { - std::string buffer; - if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer, GetDicom(dicomInstance).getDataset())) - { - throw OrthancException(ErrorCode_InternalError); - } - - if (buffer.size() == 0) - return Store(resultPublicId, dicomInstance, NULL, 0); - else - return Store(resultPublicId, dicomInstance, &buffer[0], buffer.size()); - } - - - StoreStatus ServerContext::Store(std::string& resultPublicId, - const char* dicomBuffer, - size_t dicomSize) - { - ParsedDicomFile dicom(dicomBuffer, dicomSize); - return Store(resultPublicId, dicom, dicomBuffer, dicomSize); - } - - - StoreStatus ServerContext::Store(std::string& resultPublicId, - const std::string& dicomContent) - { - if (dicomContent.size() == 0) - { - return Store(resultPublicId, NULL, 0); - } - else - { - return Store(resultPublicId, &dicomContent[0], dicomContent.size()); - } - } - - void ServerContext::SetStoreMD5ForAttachments(bool storeMD5) { LOG(INFO) << "Storing MD5 for attachments: " << (storeMD5 ? "yes" : "no"); diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/ServerContext.h --- a/OrthancServer/ServerContext.h Thu Jul 10 11:26:05 2014 +0200 +++ b/OrthancServer/ServerContext.h Thu Jul 10 11:38:46 2014 +0200 @@ -40,6 +40,8 @@ #include "ServerIndex.h" #include "ParsedDicomFile.h" #include "DicomProtocol/ReusableDicomUserConnection.h" +#include "Scheduler/ServerScheduler.h" +#include "DicomInstanceToStore.h" namespace Orthanc { @@ -64,6 +66,13 @@ virtual IDynamicObject* Provide(const std::string& id); }; + bool ApplyReceivedInstanceFilter(const Json::Value& simplified, + const std::string& remoteAet); + + void ApplyOnStoredInstance(const std::string& instanceId, + const Json::Value& simplifiedDicom, + const Json::Value& metadata); + FileStorage storage_; ServerIndex index_; CompressedFileStorageAccessor accessor_; @@ -73,11 +82,13 @@ boost::mutex dicomCacheMutex_; MemoryCache dicomCache_; ReusableDicomUserConnection scu_; + ServerScheduler scheduler_; + boost::mutex luaMutex_; LuaContext lua_; public: - class DicomCacheLocker + class DicomCacheLocker : public boost::noncopyable { private: ServerContext& that_; @@ -95,6 +106,29 @@ } }; + class LuaContextLocker : public boost::noncopyable + { + private: + ServerContext& that_; + + public: + LuaContextLocker(ServerContext& that) : that_(that) + { + that.luaMutex_.lock(); + } + + ~LuaContextLocker() + { + that_.luaMutex_.unlock(); + } + + LuaContext& GetLua() + { + return that_.lua_; + } + }; + + ServerContext(const boost::filesystem::path& storagePath, const boost::filesystem::path& indexPath); @@ -117,26 +151,8 @@ const void* data, size_t size); - StoreStatus Store(const char* dicomInstance, - size_t dicomSize, - const DicomMap& dicomSummary, - const Json::Value& dicomJson, - const std::string& remoteAet); - StoreStatus Store(std::string& resultPublicId, - ParsedDicomFile& dicomInstance, - const char* dicomBuffer, - size_t dicomSize); - - StoreStatus Store(std::string& resultPublicId, - ParsedDicomFile& dicomInstance); - - StoreStatus Store(std::string& resultPublicId, - const char* dicomBuffer, - size_t dicomSize); - - StoreStatus Store(std::string& resultPublicId, - const std::string& dicomContent); + DicomInstanceToStore& dicom); void AnswerDicomFile(RestApiOutput& output, const std::string& instancePublicId, @@ -151,11 +167,6 @@ FileContentType content, bool uncompressIfNeeded = true); - LuaContext& GetLuaContext() - { - return lua_; - } - void SetStoreMD5ForAttachments(bool storeMD5); bool IsStoreMD5ForAttachments() const @@ -167,5 +178,10 @@ { return scu_; } + + ServerScheduler& GetScheduler() + { + return scheduler_; + } }; } diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/ServerEnumerations.cpp --- a/OrthancServer/ServerEnumerations.cpp Thu Jul 10 11:26:05 2014 +0200 +++ b/OrthancServer/ServerEnumerations.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -279,6 +279,9 @@ case ModalityManufacturer_Generic: return "Generic"; + case ModalityManufacturer_StoreScp: + return "StoreScp"; + case ModalityManufacturer_ClearCanvas: return "ClearCanvas"; @@ -336,6 +339,10 @@ { return ModalityManufacturer_ClearCanvas; } + else if (manufacturer == "StoreScp") + { + return ModalityManufacturer_StoreScp; + } else if (manufacturer == "MedInria") { return ModalityManufacturer_MedInria; diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/ServerEnumerations.h --- a/OrthancServer/ServerEnumerations.h Thu Jul 10 11:26:05 2014 +0200 +++ b/OrthancServer/ServerEnumerations.h Thu Jul 10 11:38:46 2014 +0200 @@ -32,6 +32,7 @@ #pragma once #include +#include #include "../Core/Enumerations.h" @@ -56,6 +57,7 @@ enum ModalityManufacturer { ModalityManufacturer_Generic, + ModalityManufacturer_StoreScp, ModalityManufacturer_ClearCanvas, ModalityManufacturer_MedInria, ModalityManufacturer_Dcm4Chee @@ -124,6 +126,8 @@ ChangeType_StableSeries = 14 }; + + void InitializeServerEnumerations(); void RegisterUserMetadata(int metadata, diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/ServerIndex.cpp --- a/OrthancServer/ServerIndex.cpp Thu Jul 10 11:26:05 2014 +0200 +++ b/OrthancServer/ServerIndex.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -382,13 +382,17 @@ } - StoreStatus ServerIndex::Store(const DicomMap& dicomSummary, + StoreStatus ServerIndex::Store(std::map& instanceMetadata, + const DicomMap& dicomSummary, const Attachments& attachments, - const std::string& remoteAet) + const std::string& remoteAet, + const MetadataMap& metadata) { boost::mutex::scoped_lock lock(mutex_); listener_->Reset(); + instanceMetadata.clear(); + DicomInstanceHasher hasher(dicomSummary); try @@ -402,6 +406,7 @@ if (db_->LookupResource(hasher.HashInstance(), tmp, type)) { assert(type == ResourceType_Instance); + db_->GetAllMetadata(instanceMetadata, tmp); return StoreStatus_AlreadyStored; } } @@ -519,27 +524,62 @@ db_->AddAttachment(instance, *it); } - // Attach the metadata + // Attach the user-specified metadata + for (MetadataMap::const_iterator + it = metadata.begin(); it != metadata.end(); ++it) + { + switch (it->first.first) + { + case ResourceType_Patient: + db_->SetMetadata(patient, it->first.second, it->second); + break; + + case ResourceType_Study: + db_->SetMetadata(study, it->first.second, it->second); + break; + + case ResourceType_Series: + db_->SetMetadata(series, it->first.second, it->second); + break; + + case ResourceType_Instance: + db_->SetMetadata(instance, it->first.second, it->second); + instanceMetadata[it->first.second] = it->second; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + // Attach the auto-computed metadata for the patient/study/series levels std::string now = Toolbox::GetNowIsoString(); - db_->SetMetadata(instance, MetadataType_Instance_ReceptionDate, now); db_->SetMetadata(series, MetadataType_LastUpdate, now); db_->SetMetadata(study, MetadataType_LastUpdate, now); db_->SetMetadata(patient, MetadataType_LastUpdate, now); + + // Attach the auto-computed metadata for the instance level, + // reflecting these additions into the input metadata map + db_->SetMetadata(instance, MetadataType_Instance_ReceptionDate, now); + instanceMetadata[MetadataType_Instance_ReceptionDate] = now; + db_->SetMetadata(instance, MetadataType_Instance_RemoteAet, remoteAet); + instanceMetadata[MetadataType_Instance_RemoteAet] = remoteAet; const DicomValue* value; if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_INSTANCE_NUMBER)) != NULL || (value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGE_INDEX)) != NULL) { db_->SetMetadata(instance, MetadataType_Instance_IndexInSeries, value->AsString()); + instanceMetadata[MetadataType_Instance_IndexInSeries] = value->AsString(); } + // Check whether the series of this new instance is now completed if (isNewSeries) { ComputeExpectedNumberOfInstances(*db_, series, dicomSummary); } - // Check whether the series of this new instance is now completed SeriesStatus seriesStatus = GetSeriesStatus(series); if (seriesStatus == SeriesStatus_Complete) { @@ -1694,4 +1734,33 @@ } + bool ServerIndex::GetMetadata(Json::Value& target, + const std::string& publicId) + { + boost::mutex::scoped_lock lock(mutex_); + + target = Json::objectValue; + + ResourceType type; + int64_t id; + if (!db_->LookupResource(publicId, id, type)) + { + return false; + } + + std::list metadata; + db_->ListAvailableMetadata(metadata, id); + + for (std::list::const_iterator + it = metadata.begin(); it != metadata.end(); it++) + { + std::string key = EnumerationToString(*it); + std::string value = db_->GetMetadata(id, *it); + target[key] = value; + } + + return true; + } + + } diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/ServerIndex.h --- a/OrthancServer/ServerIndex.h Thu Jul 10 11:26:05 2014 +0200 +++ b/OrthancServer/ServerIndex.h Thu Jul 10 11:38:46 2014 +0200 @@ -54,6 +54,10 @@ class ServerIndex : public boost::noncopyable { + public: + typedef std::list Attachments; + typedef std::map< std::pair, std::string> MetadataMap; + private: class Transaction; struct UnstableResourcePayload; @@ -99,8 +103,6 @@ /* in */ ResourceType type); public: - typedef std::list Attachments; - ServerIndex(ServerContext& context, const std::string& dbPath); @@ -122,9 +124,11 @@ // "count == 0" means no limit on the number of patients void SetMaximumPatientCount(unsigned int count); - StoreStatus Store(const DicomMap& dicomSummary, + StoreStatus Store(std::map& instanceMetadata, + const DicomMap& dicomSummary, const Attachments& attachments, - const std::string& remoteAet); + const std::string& remoteAet, + const MetadataMap& metadata); void ComputeStatistics(Json::Value& target); @@ -183,6 +187,9 @@ void ListAvailableMetadata(std::list& target, const std::string& publicId); + bool GetMetadata(Json::Value& target, + const std::string& publicId); + void ListAvailableAttachments(std::list& target, const std::string& publicId, ResourceType expectedType); diff -r f4bbf13572cd -r 1fc112c4b832 OrthancServer/main.cpp --- a/OrthancServer/main.cpp Thu Jul 10 11:26:05 2014 +0200 +++ b/OrthancServer/main.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -73,7 +73,14 @@ { if (dicomFile.size() > 0) { - server_.Store(&dicomFile[0], dicomFile.size(), dicomSummary, dicomJson, remoteAet); + DicomInstanceToStore toStore; + toStore.SetBuffer(dicomFile); + toStore.SetSummary(dicomSummary); + toStore.SetJson(dicomJson); + toStore.SetRemoteAet(remoteAet); + + std::string id; + server_.Store(id, toStore); } } }; @@ -188,10 +195,12 @@ { static const char* HTTP_FILTER = "IncomingHttpRequestFilter"; + ServerContext::LuaContextLocker locker(context_); + // Test if the instance must be filtered out - if (context_.GetLuaContext().IsExistingFunction(HTTP_FILTER)) + if (locker.GetLua().IsExistingFunction(HTTP_FILTER)) { - LuaFunctionCall call(context_.GetLuaContext(), HTTP_FILTER); + LuaFunctionCall call(locker.GetLua(), HTTP_FILTER); switch (method) { @@ -283,7 +292,9 @@ LOG(WARNING) << "Installing the Lua scripts from: " << path; std::string script; Toolbox::ReadFile(script, path); - context.GetLuaContext().Execute(script); + + ServerContext::LuaContextLocker locker(context); + locker.GetLua().Execute(script); } } diff -r f4bbf13572cd -r 1fc112c4b832 Resources/Configuration.json --- a/Resources/Configuration.json Thu Jul 10 11:26:05 2014 +0200 +++ b/Resources/Configuration.json Thu Jul 10 11:38:46 2014 +0200 @@ -105,8 +105,9 @@ /** * A fourth parameter is available to enable patches for a * specific PACS manufacturer. The allowed values are currently - * "Generic" (default value), "ClearCanvas", "MedInria" and - * "Dcm4Chee". This parameter is case-sensitive. + * "Generic" (default value), "StoreScp" (storescp tool from + * DCMTK), "ClearCanvas", "MedInria" and "Dcm4Chee". This + * parameter is case-sensitive. **/ // "clearcanvas" : [ "CLEARCANVAS", "192.168.1.1", 104, "ClearCanvas" ] }, @@ -174,5 +175,10 @@ // The maximum number of results for a single C-FIND request at the // Instance level. Setting this option to "0" means no limit. - "LimitFindInstances" : 0 + "LimitFindInstances" : 0, + + // The maximum number of active jobs in the Orthanc scheduler. When + // this limit is reached, the addition of new jobs is blocked until + // some job finishes. + "LimitJobs" : 10 } diff -r f4bbf13572cd -r 1fc112c4b832 Resources/Samples/Lua/Autorouting.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Samples/Lua/Autorouting.lua Thu Jul 10 11:38:46 2014 +0200 @@ -0,0 +1,3 @@ +function OnStoredInstance(instanceId, tags, metadata) + Delete(SendToModality(instanceId, 'sample')) +end diff -r f4bbf13572cd -r 1fc112c4b832 Resources/Samples/Lua/AutoroutingConditional.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Samples/Lua/AutoroutingConditional.lua Thu Jul 10 11:38:46 2014 +0200 @@ -0,0 +1,13 @@ +function OnStoredInstance(instanceId, tags, metadata) + -- Extract the value of the "PatientName" DICOM tag + local patientName = string.lower(tags['PatientName']) + + if string.find(patientName, 'david') ~= nil then + -- Only send patients whose name contains "David" + Delete(SendToModality(instanceId, 'sample')) + + else + -- Delete the patients that are not called "David" + Delete(instanceId) + end +end diff -r f4bbf13572cd -r 1fc112c4b832 Resources/Samples/Lua/AutoroutingModification.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Samples/Lua/AutoroutingModification.lua Thu Jul 10 11:38:46 2014 +0200 @@ -0,0 +1,20 @@ +function OnStoredInstance(instanceId, tags, metadata) + -- Ignore the instances that result from a modification to avoid + -- infinite loops + if (metadata['ModifiedFrom'] == nil and + metadata['AnonymizedFrom'] == nil) then + + -- The tags to be replaced + local replace = {} + replace['StationName'] = 'My Medical Device' + + -- The tags to be removed + local remove = { 'MilitaryRank' } + + -- Modify the instance, send it, then delete the modified instance + Delete(SendToModality(ModifyInstance(instanceId, replace, remove, true), 'sample')) + + -- Delete the original instance + Delete(instanceId) + end +end diff -r f4bbf13572cd -r 1fc112c4b832 Resources/Toolbox.lua --- a/Resources/Toolbox.lua Thu Jul 10 11:26:05 2014 +0200 +++ b/Resources/Toolbox.lua Thu Jul 10 11:38:46 2014 +0200 @@ -6,7 +6,8 @@ --]] function PrintRecursive(s, l, i) -- recursive Print (structure, limit, indent) - l = (l) or 100; i = i or ""; -- default item limit, indent string + l = (l) or 100; -- default item limit + i = i or ""; -- indent string if (l<1) then print "ERROR: Item limit reached."; return l-1 end; local ts = type(s); if (ts ~= "table") then print (i,ts,s); return l-1 end @@ -18,4 +19,79 @@ return l end + + + +function _InitializeJob() + _job = {} +end + + +function _AccessJob() + return _job +end + + +function SendToModality(instanceId, modality) + if instanceId == nil then + error('Cannot send a nonexistent instance') + end + + table.insert(_job, { + Operation = 'store-scu', + Instance = instanceId, + Modality = modality + }) + return instanceId +end + + +function SendToPeer(instanceId, peer) + if instanceId == nil then + error('Cannot send a nonexistent instance') + end + + table.insert(_job, { + Operation = 'store-peer', + Instance = instanceId, + Peer = peer + }) + return instanceId +end + + +function Delete(instanceId) + if instanceId == nil then + error('Cannot delete a nonexistent instance') + end + + table.insert(_job, { + Operation = 'delete', + Instance = instanceId + }) + return nil -- Forbid chaining +end + + +function ModifyInstance(instanceId, replacements, removals, removePrivateTags) + if instanceId == nil then + error('Cannot modify a nonexistent instance') + end + + if instanceId == '' then + error('Cannot modify twice an instance'); + end + + table.insert(_job, { + Operation = 'modify', + Instance = instanceId, + Replace = replacements, + Remove = removals, + RemovePrivateTags = removePrivateTags + }) + return '' -- Chain with another operation +end + + + print('Lua toolbox installed') diff -r f4bbf13572cd -r 1fc112c4b832 UnitTestsSources/LuaTests.cpp --- a/UnitTestsSources/LuaTests.cpp Thu Jul 10 11:26:05 2014 +0200 +++ b/UnitTestsSources/LuaTests.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -35,6 +35,8 @@ #include "../Core/Lua/LuaFunctionCall.h" +#include + TEST(Lua, Json) { @@ -65,13 +67,13 @@ { Orthanc::LuaFunctionCall f(lua, "PrintRecursive"); - f.PushJSON(v); + f.PushJson(v); f.Execute(); } { Orthanc::LuaFunctionCall f(lua, "f"); - f.PushJSON(o); + f.PushJson(o); ASSERT_THROW(f.ExecutePredicate(), Orthanc::LuaException); } @@ -79,7 +81,7 @@ { Orthanc::LuaFunctionCall f(lua, "f"); - f.PushJSON(o); + f.PushJson(o); ASSERT_FALSE(f.ExecutePredicate()); } @@ -87,7 +89,7 @@ { Orthanc::LuaFunctionCall f(lua, "f"); - f.PushJSON(o); + f.PushJson(o); ASSERT_TRUE(f.ExecutePredicate()); } } @@ -134,3 +136,99 @@ f.Execute(); } } + + +TEST(Lua, ReturnJson) +{ + Json::Value b = Json::objectValue; + b["a"] = 42; + b["b"] = 44; + b["c"] = 43; + + Json::Value c = Json::arrayValue; + c.append("test3"); + c.append("test1"); + c.append("test2"); + + Json::Value a = Json::objectValue; + a["Hello"] = "World"; + a["List"] = Json::arrayValue; + a["List"].append(b); + a["List"].append(c); + + Orthanc::LuaContext lua; + + // This is the identity function (it simply returns its input) + lua.Execute("function identity(a) return a end"); + + { + Orthanc::LuaFunctionCall f(lua, "identity"); + f.PushJson("hello"); + Json::Value v; + f.ExecuteToJson(v); + ASSERT_EQ("hello", v.asString()); + } + + { + Orthanc::LuaFunctionCall f(lua, "identity"); + f.PushJson(42.25); + Json::Value v; + f.ExecuteToJson(v); + ASSERT_FLOAT_EQ(42.25f, v.asFloat()); + } + + { + Orthanc::LuaFunctionCall f(lua, "identity"); + Json::Value vv = Json::arrayValue; + f.PushJson(vv); + Json::Value v; + f.ExecuteToJson(v); + ASSERT_EQ(Json::arrayValue, v.type()); + } + + { + Orthanc::LuaFunctionCall f(lua, "identity"); + Json::Value vv = Json::objectValue; + f.PushJson(vv); + Json::Value v; + f.ExecuteToJson(v); + // Lua does not make the distinction between empty lists and empty objects + ASSERT_EQ(Json::arrayValue, v.type()); + } + + { + Orthanc::LuaFunctionCall f(lua, "identity"); + f.PushJson(b); + Json::Value v; + f.ExecuteToJson(v); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_FLOAT_EQ(42.0f, v["a"].asFloat()); + ASSERT_FLOAT_EQ(44.0f, v["b"].asFloat()); + ASSERT_FLOAT_EQ(43.0f, v["c"].asFloat()); + } + + { + Orthanc::LuaFunctionCall f(lua, "identity"); + f.PushJson(c); + Json::Value v; + f.ExecuteToJson(v); + ASSERT_EQ(Json::arrayValue, v.type()); + ASSERT_EQ("test3", v[0].asString()); + ASSERT_EQ("test1", v[1].asString()); + ASSERT_EQ("test2", v[2].asString()); + } + + { + Orthanc::LuaFunctionCall f(lua, "identity"); + f.PushJson(a); + Json::Value v; + f.ExecuteToJson(v); + ASSERT_EQ("World", v["Hello"].asString()); + ASSERT_EQ(42, v["List"][0]["a"].asInt()); + ASSERT_EQ(44, v["List"][0]["b"].asInt()); + ASSERT_EQ(43, v["List"][0]["c"].asInt()); + ASSERT_EQ("test3", v["List"][1][0].asString()); + ASSERT_EQ("test1", v["List"][1][1].asString()); + ASSERT_EQ("test2", v["List"][1][2].asString()); + } +} diff -r f4bbf13572cd -r 1fc112c4b832 UnitTestsSources/MultiThreadingTests.cpp --- a/UnitTestsSources/MultiThreadingTests.cpp Thu Jul 10 11:26:05 2014 +0200 +++ b/UnitTestsSources/MultiThreadingTests.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -32,8 +32,9 @@ #include "PrecompiledHeadersUnitTests.h" #include "gtest/gtest.h" +#include -#include +#include "../OrthancServer/Scheduler/ServerScheduler.h" #include "../Core/OrthancException.h" #include "../Core/Toolbox.h" @@ -248,8 +249,6 @@ - - #include "../OrthancServer/DicomProtocol/ReusableDicomUserConnection.h" TEST(ReusableDicomUserConnection, DISABLED_Basic) @@ -257,6 +256,7 @@ ReusableDicomUserConnection c; c.SetMillisecondsBeforeClose(200); printf("START\n"); fflush(stdout); + { ReusableDicomUserConnection::Locker lock(c, "STORESCP", "localhost", 2000, ModalityManufacturer_Generic); lock.GetConnection().StoreFile("/home/jodogne/DICOM/Cardiac/MR.X.1.2.276.0.7230010.3.1.4.2831157719.2256.1336386844.676281"); @@ -274,3 +274,99 @@ Toolbox::ServerBarrier(); printf("DONE\n"); fflush(stdout); } + + + +class Tutu : public IServerCommand +{ +private: + int factor_; + +public: + Tutu(int f) : factor_(f) + { + } + + virtual bool Apply(ListOfStrings& outputs, + const ListOfStrings& inputs) + { + for (ListOfStrings::const_iterator + it = inputs.begin(); it != inputs.end(); it++) + { + int a = boost::lexical_cast(*it); + int b = factor_ * a; + + printf("%d * %d = %d\n", a, factor_, b); + + //if (a == 84) { printf("BREAK\n"); return false; } + + outputs.push_back(boost::lexical_cast(b)); + } + + Toolbox::USleep(100000); + + return true; + } +}; + + +static void Tata(ServerScheduler* s, ServerJob* j, bool* done) +{ + typedef IServerCommand::ListOfStrings ListOfStrings; + + while (!(*done)) + { + ListOfStrings l; + s->GetListOfJobs(l); + for (ListOfStrings::iterator i = l.begin(); i != l.end(); i++) + printf(">> %s: %0.1f\n", i->c_str(), 100.0f * s->GetProgress(*i)); + Toolbox::USleep(10000); + } +} + + +TEST(MultiThreading, ServerScheduler) +{ + ServerScheduler scheduler(10); + + ServerJob job; + ServerCommandInstance& f2 = job.AddCommand(new Tutu(2)); + ServerCommandInstance& f3 = job.AddCommand(new Tutu(3)); + ServerCommandInstance& f4 = job.AddCommand(new Tutu(4)); + ServerCommandInstance& f5 = job.AddCommand(new Tutu(5)); + f2.AddInput(boost::lexical_cast(42)); + //f3.AddInput(boost::lexical_cast(42)); + //f4.AddInput(boost::lexical_cast(42)); + f2.ConnectOutput(f3); + f3.ConnectOutput(f4); + f4.ConnectOutput(f5); + + f3.SetConnectedToSink(true); + f5.SetConnectedToSink(true); + + job.SetDescription("tutu"); + + bool done = false; + boost::thread t(Tata, &scheduler, &job, &done); + + + //scheduler.Submit(job); + + IServerCommand::ListOfStrings l; + scheduler.SubmitAndWait(l, job); + + ASSERT_EQ(2, l.size()); + ASSERT_EQ(42 * 2 * 3, boost::lexical_cast(l.front())); + ASSERT_EQ(42 * 2 * 3 * 4 * 5, boost::lexical_cast(l.back())); + + for (IServerCommand::ListOfStrings::iterator i = l.begin(); i != l.end(); i++) + { + printf("** %s\n", i->c_str()); + } + + //Toolbox::ServerBarrier(); + //Toolbox::USleep(3000000); + + done = true; + t.join(); +} diff -r f4bbf13572cd -r 1fc112c4b832 UnitTestsSources/ServerIndexTests.cpp --- a/UnitTestsSources/ServerIndexTests.cpp Thu Jul 10 11:26:05 2014 +0200 +++ b/UnitTestsSources/ServerIndexTests.cpp Thu Jul 10 11:38:46 2014 +0200 @@ -579,7 +579,13 @@ instance.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "study-" + id); instance.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, "series-" + id); instance.SetValue(DICOM_TAG_SOP_INSTANCE_UID, "instance-" + id); - ASSERT_EQ(StoreStatus_Success, index.Store(instance, attachments, "")); + + std::map instanceMetadata; + ServerIndex::MetadataMap metadata; + ASSERT_EQ(StoreStatus_Success, index.Store(instanceMetadata, instance, attachments, "", metadata)); + ASSERT_EQ(2, instanceMetadata.size()); + ASSERT_NE(instanceMetadata.end(), instanceMetadata.find(MetadataType_Instance_RemoteAet)); + ASSERT_NE(instanceMetadata.end(), instanceMetadata.find(MetadataType_Instance_ReceptionDate)); DicomInstanceHasher hasher(instance); ids.push_back(hasher.HashPatient());