Mercurial > hg > orthanc
changeset 289:ffd98d2f0b91
merge
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Fri, 14 Dec 2012 11:22:29 +0100 |
parents | 40d3bf6cc8d9 (diff) 9cd240cfd3a6 (current diff) |
children | b3322636b06d |
files | CMakeLists.txt |
diffstat | 41 files changed, 2044 insertions(+), 345 deletions(-) [+] |
line wrap: on
line diff
--- a/CMakeLists.txt Mon Dec 10 11:00:50 2012 +0100 +++ b/CMakeLists.txt Fri Dec 14 11:22:29 2012 +0100 @@ -4,7 +4,7 @@ # Version of the build, should always be "mainline" except in release branches add_definitions( - -DORTHANC_VERSION="0.3.1" + -DORTHANC_VERSION="mainline" ) # Parameters of the build @@ -99,6 +99,7 @@ ${AUTOGENERATED_SOURCES} ${THIRD_PARTY_SOURCES} + Core/Cache/MemoryCache.cpp Core/ChunkedBuffer.cpp Core/Compression/BufferCompressor.cpp Core/Compression/ZlibCompressor.cpp @@ -182,7 +183,6 @@ include(${CMAKE_SOURCE_DIR}/Resources/CMake/GoogleTestConfiguration.cmake) add_executable(UnitTests ${GTEST_SOURCES} - UnitTests/MessageWithDestination.cpp UnitTests/RestApi.cpp UnitTests/SQLite.cpp UnitTests/SQLiteChromium.cpp @@ -190,6 +190,7 @@ UnitTests/Versions.cpp UnitTests/Zip.cpp UnitTests/FileStorage.cpp + UnitTests/MemoryCache.cpp UnitTests/main.cpp ) target_link_libraries(UnitTests ServerLibrary CoreLibrary)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/Cache/CacheIndex.h Fri Dec 14 11:22:29 2012 +0100 @@ -0,0 +1,250 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include <list> +#include <map> +#include <boost/noncopyable.hpp> +#include <cassert> + +#include "../OrthancException.h" +#include "../Toolbox.h" + +namespace Orthanc +{ + /** + * This class implements the index of a cache with least recently + * used (LRU) recycling policy. All the items of the cache index + * can be associated with a payload. + * Reference: http://stackoverflow.com/a/2504317 + **/ + template <typename T, typename Payload = NullType> + class CacheIndex : public boost::noncopyable + { + private: + typedef std::list< std::pair<T, Payload> > Queue; + typedef std::map<T, typename Queue::iterator> Index; + + Index index_; + Queue queue_; + + /** + * Internal method for debug builds to check whether the internal + * data structures are not corrupted. + **/ + void CheckInvariants() const; + + public: + /** + * Add a new element to the cache index, and make it the most + * recent element. + * \param id The ID of the element. + * \param payload The payload of the element. + **/ + void Add(T id, Payload payload = Payload()); + + /** + * When accessing an element of the cache, this method tags the + * element as the most recently used. + * \param id The most recently accessed item. + **/ + void TagAsMostRecent(T id); + + /** + * Remove an element from the cache index. + * \param id The item to remove. + **/ + Payload Invalidate(T id); + + /** + * Get the oldest element in the cache and remove it. + * \return The oldest item. + **/ + T RemoveOldest() + { + Payload p; + return RemoveOldest(p); + } + + /** + * Get the oldest element in the cache, remove it and return the + * associated payload. + * \param payload Where to store the associated payload. + * \return The oldest item. + **/ + T RemoveOldest(Payload& payload); + + /** + * Check whether an element is contained in the cache. + * \param id The item. + * \return \c true iff the item is indexed by the cache. + **/ + bool Contains(T id) const + { + return index_.find(id) != index_.end(); + } + + bool Contains(T id, Payload& payload) const + { + typename Index::const_iterator it = index_.find(id); + if (it == index_.end()) + { + return false; + } + else + { + payload = it->second->second; + return true; + } + } + + /** + * Return the number of elements in the cache. + * \return The number of elements. + **/ + size_t GetSize() const + { + assert(index_.size() == queue_.size()); + return queue_.size(); + } + + /** + * Check whether the cache index is empty. + * \return \c true iff the cache is empty. + **/ + bool IsEmpty() const + { + return index_.empty(); + } + }; + + + + + /****************************************************************** + ** Implementation of the template + ******************************************************************/ + + template <typename T, typename Payload> + void CacheIndex<T, Payload>::CheckInvariants() const + { +#ifndef NDEBUG + assert(index_.size() == queue_.size()); + + for (typename Index::const_iterator + it = index_.begin(); it != index_.end(); it++) + { + assert(it->second != queue_.end()); + assert(it->second->first == it->first); + } +#endif + } + + + template <typename T, typename Payload> + void CacheIndex<T, Payload>::Add(T id, Payload payload) + { + if (Contains(id)) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + queue_.push_front(std::make_pair(id, payload)); + index_[id] = queue_.begin(); + + CheckInvariants(); + } + + + template <typename T, typename Payload> + void CacheIndex<T, Payload>::TagAsMostRecent(T id) + { + if (!Contains(id)) + { + throw OrthancException(ErrorCode_InexistentItem); + } + + typename Index::iterator it = index_.find(id); + assert(it != index_.end()); + + std::pair<T, Payload> item = *(it->second); + + queue_.erase(it->second); + queue_.push_front(item); + index_[id] = queue_.begin(); + + CheckInvariants(); + } + + + template <typename T, typename Payload> + Payload CacheIndex<T, Payload>::Invalidate(T id) + { + if (!Contains(id)) + { + throw OrthancException(ErrorCode_InexistentItem); + } + + typename Index::iterator it = index_.find(id); + assert(it != index_.end()); + + Payload payload = it->second->second; + queue_.erase(it->second); + index_.erase(it); + + CheckInvariants(); + return payload; + } + + + template <typename T, typename Payload> + T CacheIndex<T, Payload>::RemoveOldest(Payload& payload) + { + if (IsEmpty()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + std::pair<T, Payload> item = queue_.back(); + T oldest = item.first; + payload = item.second; + + queue_.pop_back(); + assert(index_.find(oldest) != index_.end()); + index_.erase(oldest); + + CheckInvariants(); + + return oldest; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/Cache/ICachePageProvider.h Fri Dec 14 11:22:29 2012 +0100 @@ -0,0 +1,49 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include <string> +#include "../IDynamicObject.h" + +namespace Orthanc +{ + class ICachePageProvider + { + public: + virtual ~ICachePageProvider() + { + } + + virtual IDynamicObject* Provide(const std::string& id) = 0; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/Cache/MemoryCache.cpp Fri Dec 14 11:22:29 2012 +0100 @@ -0,0 +1,94 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "MemoryCache.h" + +#include <glog/logging.h> + +namespace Orthanc +{ + MemoryCache::Page& MemoryCache::Load(const std::string& id) + { + // Reuse the cache entry if it already exists + Page* p = NULL; + if (index_.Contains(id, p)) + { + VLOG(1) << "Reusing a cache page"; + assert(p != NULL); + index_.TagAsMostRecent(id); + return *p; + } + + // The id is not in the cache yet. Make some room if the cache + // is full. + if (index_.GetSize() == cacheSize_) + { + VLOG(1) << "Dropping the oldest cache page"; + index_.RemoveOldest(p); + delete p; + } + + // Create a new cache page + std::auto_ptr<Page> result(new Page); + result->id_ = id; + result->content_.reset(provider_.Provide(id)); + + // Add the newly create page to the cache + VLOG(1) << "Registering new data in a cache page"; + p = result.release(); + index_.Add(id, p); + return *p; + } + + MemoryCache::MemoryCache(ICachePageProvider& provider, + size_t cacheSize) : + provider_(provider), + cacheSize_(cacheSize) + { + } + + MemoryCache::~MemoryCache() + { + while (!index_.IsEmpty()) + { + Page* element = NULL; + index_.RemoveOldest(element); + assert(element != NULL); + delete element; + } + } + + IDynamicObject& MemoryCache::Access(const std::string& id) + { + return *Load(id).content_; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/Cache/MemoryCache.h Fri Dec 14 11:22:29 2012 +0100 @@ -0,0 +1,67 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include <memory> +#include "CacheIndex.h" +#include "ICachePageProvider.h" + +namespace Orthanc +{ + /** + * WARNING: This class is NOT thread-safe. + **/ + class MemoryCache + { + private: + struct Page + { + std::string id_; + std::auto_ptr<IDynamicObject> content_; + }; + + ICachePageProvider& provider_; + size_t cacheSize_; + CacheIndex<std::string, Page*> index_; + + Page& Load(const std::string& id); + + public: + MemoryCache(ICachePageProvider& provider, + size_t cacheSize); + + ~MemoryCache(); + + IDynamicObject& Access(const std::string& id); + }; +}
--- a/Core/Enumerations.h Mon Dec 10 11:00:50 2012 +0100 +++ b/Core/Enumerations.h Fri Dec 14 11:22:29 2012 +0100 @@ -47,6 +47,7 @@ ErrorCode_NotEnoughMemory, ErrorCode_BadParameterType, ErrorCode_BadSequenceOfCalls, + ErrorCode_InexistentItem, // Specific error codes ErrorCode_UriSyntax, @@ -55,7 +56,8 @@ ErrorCode_BadFileFormat, ErrorCode_Timeout, ErrorCode_UnknownResource, - ErrorCode_IncompatibleDatabaseVersion + ErrorCode_IncompatibleDatabaseVersion, + ErrorCode_FullStorage }; enum PixelFormat
--- a/Core/HttpServer/HttpOutput.h Mon Dec 10 11:00:50 2012 +0100 +++ b/Core/HttpServer/HttpOutput.h Fri Dec 14 11:22:29 2012 +0100 @@ -43,11 +43,6 @@ private: void SendHeaderInternal(Orthanc_HttpStatus status); - void SendOkHeader(const char* contentType, - bool hasContentLength, - uint64_t contentLength, - const char* contentFilename); - public: virtual ~HttpOutput() { @@ -55,6 +50,11 @@ virtual void Send(const void* buffer, size_t length) = 0; + void SendOkHeader(const char* contentType, + bool hasContentLength, + uint64_t contentLength, + const char* contentFilename); + void SendCustomOkHeader(const std::string& customHeader); void SendString(const std::string& s);
--- a/Core/OrthancException.cpp Mon Dec 10 11:00:50 2012 +0100 +++ b/Core/OrthancException.cpp Fri Dec 14 11:22:29 2012 +0100 @@ -93,6 +93,12 @@ case ErrorCode_IncompatibleDatabaseVersion: return "Incompatible version of the database"; + case ErrorCode_FullStorage: + return "The file storage is full"; + + case ErrorCode_InexistentItem: + return "Accessing an inexistent item"; + case ErrorCode_Custom: default: return "???";
--- a/Core/RestApi/RestApi.h Mon Dec 10 11:00:50 2012 +0100 +++ b/Core/RestApi/RestApi.h Fri Dec 14 11:22:29 2012 +0100 @@ -70,6 +70,11 @@ { return *fullUri_; } + + const UriComponents& GetTrailingUri() const + { + return *trailing_; + } std::string GetUriComponent(const std::string& name, const std::string& defaultValue) const
--- a/Core/RestApi/RestApiOutput.h Mon Dec 10 11:00:50 2012 +0100 +++ b/Core/RestApi/RestApiOutput.h Fri Dec 14 11:22:29 2012 +0100 @@ -52,6 +52,16 @@ ~RestApiOutput(); + HttpOutput& GetLowLevelOutput() + { + return output_; + } + + void MarkLowLevelOutputDone() + { + alreadySent_ = true; + } + void AnswerFile(HttpFileSender& sender); void AnswerJson(const Json::Value& value);
--- a/Core/SQLite/FunctionContext.cpp Mon Dec 10 11:00:50 2012 +0100 +++ b/Core/SQLite/FunctionContext.cpp Fri Dec 14 11:22:29 2012 +0100 @@ -72,6 +72,12 @@ return sqlite3_value_int(argv_[index]); } + int64_t FunctionContext::GetInt64Value(unsigned int index) const + { + CheckIndex(index); + return sqlite3_value_int64(argv_[index]); + } + double FunctionContext::GetDoubleValue(unsigned int index) const { CheckIndex(index);
--- a/Core/SQLite/FunctionContext.h Mon Dec 10 11:00:50 2012 +0100 +++ b/Core/SQLite/FunctionContext.h Fri Dec 14 11:22:29 2012 +0100 @@ -69,6 +69,8 @@ int GetIntValue(unsigned int index) const; + int64_t GetInt64Value(unsigned int index) const; + double GetDoubleValue(unsigned int index) const; std::string GetStringValue(unsigned int index) const;
--- a/Core/Toolbox.h Mon Dec 10 11:00:50 2012 +0100 +++ b/Core/Toolbox.h Fri Dec 14 11:22:29 2012 +0100 @@ -40,6 +40,10 @@ { typedef std::vector<std::string> UriComponents; + class NullType + { + }; + namespace Toolbox { void ServerBarrier();
--- a/INSTALL Mon Dec 10 11:00:50 2012 +0100 +++ b/INSTALL Fri Dec 14 11:22:29 2012 +0100 @@ -67,7 +67,7 @@ # cmake -DSTATIC_BUILD=OFF -DCMAKE_BUILD_TYPE=DEBUG ~/Orthanc # make - + Cross-Compilation for Windows under Linux ----------------------------------------- @@ -104,3 +104,17 @@ Visual Studio 2005: http://en.wikipedia.org/wiki/Microsoft_Windows_SDK. Read the CMake FAQ: http://goo.gl/By90B + + + +Debian/Ubuntu specific +---------------------- + +When dynamically linking against the system libraries, you have to +manually add the "wrap" and "oflog" libraries at the configuration +time (because of a packaging error in "libdcmtk"): + +# cd ~/OrthancBuild +# cmake "-DDCMTK_LIBRARIES=wrap;oflog" -DSTATIC_BUILD=OFF -DCMAKE_BUILD_TYPE=DEBUG ~/Orthanc +# make +
--- a/NEWS Mon Dec 10 11:00:50 2012 +0100 +++ b/NEWS Fri Dec 14 11:22:29 2012 +0100 @@ -1,6 +1,9 @@ Pending changes in the mainline =============================== +* Recycling of disk space +* Protection of patients against recycling (also in Orthanc Explorer) +* Raw access to the value of the DICOM tags in the REST API Version 0.3.1 (2012/12/05)
--- a/OrthancExplorer/explorer.css Mon Dec 10 11:00:50 2012 +0100 +++ b/OrthancExplorer/explorer.css Fri Dec 14 11:22:29 2012 +0100 @@ -37,3 +37,7 @@ text-decoration: none; color: white !important; } + +.switch-container .ui-slider-switch { + width: 100%; +} \ No newline at end of file
--- a/OrthancExplorer/explorer.html Mon Dec 10 11:00:50 2012 +0100 +++ b/OrthancExplorer/explorer.html Fri Dec 14 11:22:29 2012 +0100 @@ -82,7 +82,13 @@ <ul data-role="listview" data-inset="true" data-theme="a" id="patient-info"> </ul> <p> - <a href="#find-patients" data-role="button" data-icon="search">Go to patient finder</a> + <div class="switch-container"> + <select name="protection" id="protection" data-role="slider"> + <option value="off">Unprotected</option> + <option value="on">Protected</option> + </select> + </div> + <!--a href="#find-patients" data-role="button" data-icon="search">Go to patient finder</a--> <a href="#" data-role="button" data-icon="delete" id="patient-delete">Delete this patient</a> <a href="#" data-role="button" data-icon="gear" id="patient-archive">Download ZIP</a> </p>
--- a/OrthancExplorer/explorer.js Mon Dec 10 11:00:50 2012 +0100 +++ b/OrthancExplorer/explorer.js Fri Dec 14 11:22:29 2012 +0100 @@ -378,6 +378,18 @@ } target.listview('refresh'); + + // Check whether this patient is protected + $.ajax({ + url: '../patients/' + $.mobile.pageData.uuid + '/protected', + type: 'GET', + dataType: 'text', + async: false, + success: function (s) { + var v = (s == '1') ? 'on' : 'off'; + $('#protection').val(v).slider('refresh'); + } + }); }); }); } @@ -786,3 +798,13 @@ window.location.href = '../series/' + $.mobile.pageData.uuid + '/archive'; }); +$('#protection').live('change', function(e) { + var isProtected = e.target.value == "on"; + $.ajax({ + url: '../patients/' + $.mobile.pageData.uuid + '/protected', + type: 'PUT', + dataType: 'text', + data: isProtected ? '1' : '0', + async: false + }); +});
--- a/OrthancServer/DatabaseWrapper.cpp Mon Dec 10 11:00:50 2012 +0100 +++ b/OrthancServer/DatabaseWrapper.cpp Fri Dec 14 11:22:29 2012 +0100 @@ -33,6 +33,7 @@ #include "DatabaseWrapper.h" #include "../Core/DicomFormat/DicomArray.h" +#include "../Core/Uuid.h" #include "EmbeddedResources.h" #include <glog/logging.h> @@ -61,12 +62,18 @@ virtual unsigned int GetCardinality() const { - return 1; + return 5; } virtual void Compute(SQLite::FunctionContext& context) { - listener_.SignalFileDeleted(context.GetStringValue(0)); + FileInfo info(context.GetStringValue(0), + static_cast<FileContentType>(context.GetIntValue(1)), + static_cast<uint64_t>(context.GetInt64Value(2)), + static_cast<CompressionType>(context.GetIntValue(3)), + static_cast<uint64_t>(context.GetInt64Value(4))); + + listener_.SignalFileDeleted(info); } }; @@ -743,9 +750,9 @@ LOG(INFO) << "Version of the Orthanc database: " << version; unsigned int v = boost::lexical_cast<unsigned int>(version); - // This version of Orthanc is only compatible with version 2 of - // the DB schema (since Orthanc 0.3.1) - ok = (v == 2); + // This version of Orthanc is only compatible with version 3 of + // the DB schema (since Orthanc 0.3.2) + ok = (v == 3); } catch (boost::bad_lexical_cast&) { @@ -777,4 +784,70 @@ return c; } + + bool DatabaseWrapper::SelectPatientToRecycle(int64_t& internalId) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT patientId FROM PatientRecyclingOrder ORDER BY seq ASC LIMIT 1"); + + if (!s.Step()) + { + // No patient remaining or all the patients are protected + return false; + } + else + { + internalId = s.ColumnInt(0); + return true; + } + } + + bool DatabaseWrapper::SelectPatientToRecycle(int64_t& internalId, + int64_t patientIdToAvoid) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT patientId FROM PatientRecyclingOrder " + "WHERE patientId != ? ORDER BY seq ASC LIMIT 1"); + s.BindInt(0, patientIdToAvoid); + + if (!s.Step()) + { + // No patient remaining or all the patients are protected + return false; + } + else + { + internalId = s.ColumnInt(0); + return true; + } + } + + bool DatabaseWrapper::IsProtectedPatient(int64_t internalId) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT * FROM PatientRecyclingOrder WHERE patientId = ?"); + s.BindInt(0, internalId); + return !s.Step(); + } + + void DatabaseWrapper::SetProtectedPatient(int64_t internalId, + bool isProtected) + { + if (isProtected) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM PatientRecyclingOrder WHERE patientId=?"); + s.BindInt(0, internalId); + s.Run(); + } + else if (IsProtectedPatient(internalId)) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)"); + s.BindInt(0, internalId); + s.Run(); + } + else + { + // Nothing to do: The patient is already unprotected + } + } }
--- a/OrthancServer/DatabaseWrapper.h Mon Dec 10 11:00:50 2012 +0100 +++ b/OrthancServer/DatabaseWrapper.h Fri Dec 14 11:22:29 2012 +0100 @@ -179,6 +179,16 @@ void GetAllPublicIds(Json::Value& target, ResourceType resourceType); + bool SelectPatientToRecycle(int64_t& internalId); + + bool SelectPatientToRecycle(int64_t& internalId, + int64_t patientIdToAvoid); + + bool IsProtectedPatient(int64_t internalId); + + void SetProtectedPatient(int64_t internalId, + bool isProtected); + DatabaseWrapper(const std::string& path, IServerIndexListener& listener);
--- a/OrthancServer/FromDcmtkBridge.cpp Mon Dec 10 11:00:50 2012 +0100 +++ b/OrthancServer/FromDcmtkBridge.cpp Fri Dec 14 11:22:29 2012 +0100 @@ -63,6 +63,193 @@ namespace Orthanc { + ParsedDicomFile::ParsedDicomFile(const std::string& content) + { + DcmInputBufferStream is; + if (content.size() > 0) + { + is.setBuffer(&content[0], content.size()); + } + is.setEos(); + + file_.reset(new DcmFileFormat); + if (!file_->read(is).good()) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + + + static void SendPathValueForDictionary(RestApiOutput& output, + DcmItem& dicom) + { + Json::Value v = Json::arrayValue; + + for (unsigned long i = 0; i < dicom.card(); i++) + { + DcmElement* element = dicom.getElement(i); + if (element) + { + char buf[16]; + sprintf(buf, "%04x-%04x", element->getTag().getGTag(), element->getTag().getETag()); + v.append(buf); + } + } + + output.AnswerJson(v); + } + + static inline uint16_t GetCharValue(char c) + { + if (c >= '0' && c <= '9') + return c - '0'; + else if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + else if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + else + return 0; + } + + static inline uint16_t GetTagValue(const char* c) + { + return ((GetCharValue(c[0]) << 12) + + (GetCharValue(c[1]) << 8) + + (GetCharValue(c[2]) << 4) + + GetCharValue(c[3])); + } + + static bool ParseTagAndGroup(DcmTagKey& key, + const std::string& tag) + { + if (tag.size() != 9 || + !isxdigit(tag[0]) || + !isxdigit(tag[1]) || + !isxdigit(tag[2]) || + !isxdigit(tag[3]) || + tag[4] != '-' || + !isxdigit(tag[5]) || + !isxdigit(tag[6]) || + !isxdigit(tag[7]) || + !isxdigit(tag[8])) + { + return false; + } + + uint16_t group = GetTagValue(tag.c_str()); + uint16_t element = GetTagValue(tag.c_str() + 5); + + key = DcmTagKey(group, element); + + return true; + } + + static void SendPathValueForLeaf(RestApiOutput& output, + const std::string& tag, + DcmItem& dicom) + { + DcmTagKey k; + if (!ParseTagAndGroup(k, tag)) + { + return; + } + + DcmElement* element = NULL; + if (dicom.findAndGetElement(k, element).good() && element != NULL) + { + if (element->getVR() == EVR_SQ) + { + // This element is a sequence + Json::Value v = Json::arrayValue; + DcmSequenceOfItems& sequence = dynamic_cast<DcmSequenceOfItems&>(*element); + + for (unsigned long i = 0; i < sequence.card(); i++) + { + v.append(boost::lexical_cast<std::string>(i)); + } + + output.AnswerJson(v); + } + else + { + // This element is not a sequence + std::string buffer; + buffer.resize(65536); + Uint32 length = element->getLength(); + Uint32 offset = 0; + + output.GetLowLevelOutput().SendOkHeader("application/octet-stream", true, length, NULL); + + while (offset < length) + { + Uint32 nbytes; + if (length - offset < buffer.size()) + { + nbytes = length - offset; + } + else + { + nbytes = buffer.size(); + } + + if (element->getPartialValue(&buffer[0], offset, nbytes).good()) + { + output.GetLowLevelOutput().Send(&buffer[0], nbytes); + offset += nbytes; + } + else + { + return; + } + } + + output.MarkLowLevelOutputDone(); + } + } + } + + void ParsedDicomFile::SendPathValue(RestApiOutput& output, + const UriComponents& uri) + { + DcmItem* dicom = file_->getDataset(); + + // Go down in the tag hierarchy according to the URI + for (size_t pos = 0; pos < uri.size() / 2; pos++) + { + size_t index; + try + { + index = boost::lexical_cast<size_t>(uri[2 * pos + 1]); + } + catch (boost::bad_lexical_cast&) + { + return; + } + + DcmTagKey k; + DcmItem *child = NULL; + if (!ParseTagAndGroup(k, uri[2 * pos]) || + !dicom->findAndGetSequenceItem(k, child, index).good() || + child == NULL) + { + return; + } + + dicom = child; + } + + // We have reached the end of the URI + if (uri.size() % 2 == 0) + { + SendPathValueForDictionary(output, *dicom); + } + else + { + SendPathValueForLeaf(output, uri.back(), *dicom); + } + } + + void FromDcmtkBridge::Convert(DicomMap& target, DcmDataset& dataset) { target.Clear();
--- a/OrthancServer/FromDcmtkBridge.h Mon Dec 10 11:00:50 2012 +0100 +++ b/OrthancServer/FromDcmtkBridge.h Fri Dec 14 11:22:29 2012 +0100 @@ -33,8 +33,13 @@ #pragma once #include "../Core/DicomFormat/DicomMap.h" +#include "../Core/RestApi/RestApiOutput.h" +#include "../Core/Toolbox.h" + #include <dcmtk/dcmdata/dcdatset.h> +#include <dcmtk/dcmdata/dcfilefo.h> #include <json/json.h> +#include <memory> namespace Orthanc { @@ -52,6 +57,23 @@ DicomRootLevel_Instance }; + class ParsedDicomFile : public IDynamicObject + { + private: + std::auto_ptr<DcmFileFormat> file_; + + public: + ParsedDicomFile(const std::string& content); + + DcmFileFormat& GetDicom() + { + return *file_; + } + + void SendPathValue(RestApiOutput& output, + const UriComponents& uri); + }; + class FromDcmtkBridge { public:
--- a/OrthancServer/IServerIndexListener.h Mon Dec 10 11:00:50 2012 +0100 +++ b/OrthancServer/IServerIndexListener.h Fri Dec 14 11:22:29 2012 +0100 @@ -47,7 +47,6 @@ virtual void SignalRemainingAncestor(ResourceType parentType, const std::string& publicId) = 0; - virtual void SignalFileDeleted(const std::string& fileUuid) = 0; - + virtual void SignalFileDeleted(const FileInfo& info) = 0; }; }
--- a/OrthancServer/OrthancInitialization.h Mon Dec 10 11:00:50 2012 +0100 +++ b/OrthancServer/OrthancInitialization.h Fri Dec 14 11:22:29 2012 +0100 @@ -35,6 +35,7 @@ #include <string> #include <set> #include <json/json.h> +#include <stdint.h> #include "../Core/HttpServer/MongooseServer.h" namespace Orthanc
--- a/OrthancServer/OrthancRestApi.cpp Mon Dec 10 11:00:50 2012 +0100 +++ b/OrthancServer/OrthancRestApi.cpp Fri Dec 14 11:22:29 2012 +0100 @@ -607,6 +607,40 @@ } + // Get information about a single patient ----------------------------------- + + static void IsProtectedPatient(RestApi::GetCall& call) + { + RETRIEVE_CONTEXT(call); + std::string publicId = call.GetUriComponent("id", ""); + bool isProtected = context.GetIndex().IsProtectedPatient(publicId); + call.GetOutput().AnswerBuffer(isProtected ? "1" : "0", "text/plain"); + } + + + static void SetPatientProtection(RestApi::PutCall& call) + { + RETRIEVE_CONTEXT(call); + std::string publicId = call.GetUriComponent("id", ""); + std::string s = Toolbox::StripSpaces(call.GetPutBody()); + + if (s == "0") + { + context.GetIndex().SetProtectedPatient(publicId, false); + call.GetOutput().AnswerBuffer("", "text/plain"); + } + else if (s == "1") + { + context.GetIndex().SetProtectedPatient(publicId, true); + call.GetOutput().AnswerBuffer("", "text/plain"); + } + else + { + // Bad request + } + } + + // Get information about a single instance ---------------------------------- static void GetInstanceFile(RestApi::GetCall& call) @@ -813,6 +847,23 @@ + // Raw access to the DICOM tags of an instance ------------------------------ + + static void GetRawContent(RestApi::GetCall& call) + { + // TODO IMPROVE MULTITHREADING + static boost::mutex mutex_; + boost::mutex::scoped_lock lock(mutex_); + + RETRIEVE_CONTEXT(call); + std::string id = call.GetUriComponent("id", ""); + ParsedDicomFile& dicom = context.GetDicomFile(id); + dicom.SendPathValue(call.GetOutput(), call.GetTrailingUri()); + } + + + + // Registration of the various REST handlers -------------------------------- OrthancRestApi::OrthancRestApi(ServerContext& context) : @@ -845,10 +896,13 @@ Register("/studies/{id}/archive", GetArchive<ResourceType_Study>); Register("/series/{id}/archive", GetArchive<ResourceType_Series>); + Register("/patients/{id}/protected", IsProtectedPatient); + Register("/patients/{id}/protected", SetPatientProtection); Register("/instances/{id}/file", GetInstanceFile); Register("/instances/{id}/tags", GetInstanceTags<false>); Register("/instances/{id}/simplified-tags", GetInstanceTags<true>); Register("/instances/{id}/frames", ListFrames); + Register("/instances/{id}/content/*", GetRawContent); Register("/instances/{id}/frames/{frame}/preview", GetImage<ImageExtractionMode_Preview>); Register("/instances/{id}/frames/{frame}/image-uint8", GetImage<ImageExtractionMode_UInt8>);
--- a/OrthancServer/PrepareDatabase.sql Mon Dec 10 11:00:50 2012 +0100 +++ b/OrthancServer/PrepareDatabase.sql Fri Dec 14 11:22:29 2012 +0100 @@ -55,9 +55,15 @@ date TEXT ); +CREATE TABLE PatientRecyclingOrder( + seq INTEGER PRIMARY KEY AUTOINCREMENT, + patientId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE + ); + CREATE INDEX ChildrenIndex ON Resources(parentId); CREATE INDEX PublicIndex ON Resources(publicId); CREATE INDEX ResourceTypeIndex ON Resources(resourceType); +CREATE INDEX PatientRecyclingIndex ON PatientRecyclingOrder(patientId); CREATE INDEX MainDicomTagsIndex1 ON MainDicomTags(id); CREATE INDEX MainDicomTagsIndex2 ON MainDicomTags(tagGroup, tagElement); @@ -68,7 +74,8 @@ CREATE TRIGGER AttachedFileDeleted AFTER DELETE ON AttachedFiles BEGIN - SELECT SignalFileDeleted(old.uuid); + SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, + old.compressionType, old.compressedSize); END; CREATE TRIGGER ResourceDeleted @@ -86,6 +93,14 @@ DELETE FROM Resources WHERE internalId = old.parentId; END; +CREATE TRIGGER PatientAdded +AFTER INSERT ON Resources +FOR EACH ROW WHEN new.resourceType = 1 -- "1" corresponds to "ResourceType_Patient" in C++ +BEGIN + INSERT INTO PatientRecyclingOrder VALUES (NULL, new.internalId); +END; + + -- Set the version of the database schema -- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration -INSERT INTO GlobalProperties VALUES (1, "2"); +INSERT INTO GlobalProperties VALUES (1, "3");
--- a/OrthancServer/ServerContext.cpp Mon Dec 10 11:00:50 2012 +0100 +++ b/OrthancServer/ServerContext.cpp Fri Dec 14 11:22:29 2012 +0100 @@ -37,6 +37,8 @@ #include <glog/logging.h> +static const size_t DICOM_CACHE_SIZE = 2; + /** * IMPORTANT: We make the assumption that the same instance of * FileStorage can be accessed from multiple threads. This seems OK @@ -51,7 +53,9 @@ ServerContext::ServerContext(const boost::filesystem::path& path) : storage_(path.string()), index_(*this, path.string()), - accessor_(storage_) + accessor_(storage_), + provider_(*this), + dicomCache_(provider_, DICOM_CACHE_SIZE) { } @@ -162,4 +166,18 @@ accessor_.SetCompressionForNextOperations(attachment.GetCompressionType()); accessor_.Read(result, attachment.GetUuid()); } + + + IDynamicObject* ServerContext::DicomCacheProvider::Provide(const std::string& instancePublicId) + { + std::string content; + context_.ReadFile(content, instancePublicId, FileContentType_Dicom); + return new ParsedDicomFile(content); + } + + + ParsedDicomFile& ServerContext::GetDicomFile(const std::string& instancePublicId) + { + return dynamic_cast<ParsedDicomFile&>(dicomCache_.Access(instancePublicId)); + } }
--- a/OrthancServer/ServerContext.h Mon Dec 10 11:00:50 2012 +0100 +++ b/OrthancServer/ServerContext.h Fri Dec 14 11:22:29 2012 +0100 @@ -32,10 +32,12 @@ #pragma once -#include "ServerIndex.h" +#include "../Core/Cache/MemoryCache.h" #include "../Core/FileStorage/CompressedFileStorageAccessor.h" #include "../Core/FileStorage/FileStorage.h" #include "../Core/RestApi/RestApiOutput.h" +#include "ServerIndex.h" +#include "FromDcmtkBridge.h" namespace Orthanc { @@ -47,10 +49,26 @@ class ServerContext { private: + class DicomCacheProvider : public ICachePageProvider + { + private: + ServerContext& context_; + + public: + DicomCacheProvider(ServerContext& context) : context_(context) + { + } + + virtual IDynamicObject* Provide(const std::string& id); + }; + FileStorage storage_; ServerIndex index_; CompressedFileStorageAccessor accessor_; bool compressionEnabled_; + + DicomCacheProvider provider_; + MemoryCache dicomCache_; public: ServerContext(const boost::filesystem::path& path); @@ -86,5 +104,8 @@ void ReadFile(std::string& result, const std::string& instancePublicId, FileContentType content); + + // TODO IMPLEMENT MULTITHREADING FOR THIS METHOD + ParsedDicomFile& GetDicomFile(const std::string& instancePublicId); }; }
--- a/OrthancServer/ServerIndex.cpp Mon Dec 10 11:00:50 2012 +0100 +++ b/OrthancServer/ServerIndex.cpp Fri Dec 14 11:22:29 2012 +0100 @@ -59,12 +59,14 @@ bool hasRemainingLevel_; ResourceType remainingType_; std::string remainingPublicId_; + std::list<std::string> pendingFilesToRemove_; + uint64_t sizeOfFilesToRemove_; public: ServerIndexListener(ServerContext& context) : - context_(context), - hasRemainingLevel_(false) + context_(context) { + Reset(); assert(ResourceType_Patient < ResourceType_Study && ResourceType_Study < ResourceType_Series && ResourceType_Series < ResourceType_Instance); @@ -72,7 +74,24 @@ void Reset() { + sizeOfFilesToRemove_ = 0; hasRemainingLevel_ = false; + pendingFilesToRemove_.clear(); + } + + uint64_t GetSizeOfFilesToRemove() + { + return sizeOfFilesToRemove_; + } + + void CommitFilesToRemove() + { + for (std::list<std::string>::iterator + it = pendingFilesToRemove_.begin(); + it != pendingFilesToRemove_.end(); it++) + { + context_.RemoveFile(*it); + } } virtual void SignalRemainingAncestor(ResourceType parentType, @@ -96,10 +115,11 @@ } } - virtual void SignalFileDeleted(const std::string& fileUuid) + virtual void SignalFileDeleted(const FileInfo& info) { - assert(Toolbox::IsUuid(fileUuid)); - context_.RemoveFile(fileUuid); + assert(Toolbox::IsUuid(info.GetUuid())); + pendingFilesToRemove_.push_back(info.GetUuid()); + sizeOfFilesToRemove_ += info.GetCompressedSize(); } bool HasRemainingLevel() const @@ -122,16 +142,57 @@ } + class ServerIndex::Transaction + { + private: + ServerIndex& index_; + std::auto_ptr<SQLite::Transaction> transaction_; + bool isCommitted_; + + public: + Transaction(ServerIndex& index) : + index_(index), + isCommitted_(false) + { + assert(index_.currentStorageSize_ == index_.db_->GetTotalCompressedSize()); + + index_.listener_->Reset(); + transaction_.reset(index_.db_->StartTransaction()); + transaction_->Begin(); + } + + void Commit(uint64_t sizeOfAddedFiles) + { + if (!isCommitted_) + { + transaction_->Commit(); + + // We can remove the files once the SQLite transaction has + // been successfully committed. Some files might have to be + // deleted because of recycling. + index_.listener_->CommitFilesToRemove(); + + index_.currentStorageSize_ += sizeOfAddedFiles; + + assert(index_.currentStorageSize_ >= index_.listener_->GetSizeOfFilesToRemove()); + index_.currentStorageSize_ -= index_.listener_->GetSizeOfFilesToRemove(); + + assert(index_.currentStorageSize_ == index_.db_->GetTotalCompressedSize()); + + isCommitted_ = true; + } + } + }; + + bool ServerIndex::DeleteResource(Json::Value& target, const std::string& uuid, ResourceType expectedType) { boost::mutex::scoped_lock lock(mutex_); - listener_->Reset(); - std::auto_ptr<SQLite::Transaction> t(db_->StartTransaction()); - t->Begin(); + Transaction t(*this); int64_t id; ResourceType type; @@ -158,7 +219,7 @@ target["RemainingAncestor"] = Json::nullValue; } - t->Commit(); + t.Commit(0); return true; } @@ -180,7 +241,9 @@ ServerIndex::ServerIndex(ServerContext& context, - const std::string& dbPath) : mutex_() + const std::string& dbPath) : + maximumStorageSize_(0), + maximumPatients_(0) { listener_.reset(new Internals::ServerIndexListener(context)); @@ -203,6 +266,12 @@ db_.reset(new DatabaseWrapper(p.string() + "/index", *listener_)); } + currentStorageSize_ = db_->GetTotalCompressedSize(); + + // Initial recycling if the parameters have changed since the last + // execution of Orthanc + StandaloneRecycling(); + unsigned int sleep; try { @@ -232,13 +301,13 @@ const std::string& remoteAet) { boost::mutex::scoped_lock lock(mutex_); + listener_->Reset(); DicomInstanceHasher hasher(dicomSummary); try { - std::auto_ptr<SQLite::Transaction> t(db_->StartTransaction()); - t->Begin(); + Transaction t(*this); int64_t patient, study, series, instance; ResourceType type; @@ -251,6 +320,16 @@ return StoreStatus_AlreadyStored; } + // Ensure there is enough room in the storage for the new instance + uint64_t instanceSize = 0; + for (Attachments::const_iterator it = attachments.begin(); + it != attachments.end(); it++) + { + instanceSize += it->GetCompressedSize(); + } + + Recycle(instanceSize, hasher.HashPatient()); + // Create the instance instance = db_->CreateResource(hasher.HashInstance(), ResourceType_Instance); @@ -337,13 +416,14 @@ db_->LogChange(ChangeType_CompletedSeries, series, ResourceType_Series); } - t->Commit(); + t.Commit(instanceSize); return StoreStatus_Success; } catch (OrthancException& e) { - LOG(ERROR) << "EXCEPTION2 [" << e.What() << "]" << " " << db_->GetErrorMessage(); + LOG(ERROR) << "EXCEPTION [" << e.What() << "]" + << " (SQLite status: " << db_->GetErrorMessage() << ")"; } return StoreStatus_Failure; @@ -357,7 +437,8 @@ boost::mutex::scoped_lock lock(mutex_); target = Json::objectValue; - uint64_t cs = db_->GetTotalCompressedSize(); + uint64_t cs = currentStorageSize_; + assert(cs == db_->GetTotalCompressedSize()); uint64_t us = db_->GetTotalUncompressedSize(); target["TotalDiskSpace"] = boost::lexical_cast<std::string>(cs); target["TotalUncompressedSize"] = boost::lexical_cast<std::string>(us); @@ -477,20 +558,20 @@ switch (type) { - case ResourceType_Study: - result["ParentPatient"] = parent; - break; + case ResourceType_Study: + result["ParentPatient"] = parent; + break; - case ResourceType_Series: - result["ParentStudy"] = parent; - break; + case ResourceType_Series: + result["ParentStudy"] = parent; + break; - case ResourceType_Instance: - result["ParentSeries"] = parent; - break; + case ResourceType_Instance: + result["ParentSeries"] = parent; + break; - default: - throw OrthancException(ErrorCode_InternalError); + default: + throw OrthancException(ErrorCode_InternalError); } } @@ -510,72 +591,72 @@ switch (type) { - case ResourceType_Patient: - result["Studies"] = c; - break; + case ResourceType_Patient: + result["Studies"] = c; + break; - case ResourceType_Study: - result["Series"] = c; - break; + case ResourceType_Study: + result["Series"] = c; + break; - case ResourceType_Series: - result["Instances"] = c; - break; + case ResourceType_Series: + result["Instances"] = c; + break; - default: - throw OrthancException(ErrorCode_InternalError); + default: + throw OrthancException(ErrorCode_InternalError); } } // Set the resource type switch (type) { - case ResourceType_Patient: - result["Type"] = "Patient"; - break; + case ResourceType_Patient: + result["Type"] = "Patient"; + break; - case ResourceType_Study: - result["Type"] = "Study"; - break; - - case ResourceType_Series: - { - result["Type"] = "Series"; - result["Status"] = ToString(GetSeriesStatus(id)); + case ResourceType_Study: + result["Type"] = "Study"; + break; - int i; - if (db_->GetMetadataAsInteger(i, id, MetadataType_Series_ExpectedNumberOfInstances)) - result["ExpectedNumberOfInstances"] = i; - else - result["ExpectedNumberOfInstances"] = Json::nullValue; - - break; - } + case ResourceType_Series: + { + result["Type"] = "Series"; + result["Status"] = ToString(GetSeriesStatus(id)); - case ResourceType_Instance: - { - result["Type"] = "Instance"; + int i; + if (db_->GetMetadataAsInteger(i, id, MetadataType_Series_ExpectedNumberOfInstances)) + result["ExpectedNumberOfInstances"] = i; + else + result["ExpectedNumberOfInstances"] = Json::nullValue; - FileInfo attachment; - if (!db_->LookupAttachment(attachment, id, FileContentType_Dicom)) - { - throw OrthancException(ErrorCode_InternalError); + break; } - result["FileSize"] = static_cast<unsigned int>(attachment.GetUncompressedSize()); - result["FileUuid"] = attachment.GetUuid(); + case ResourceType_Instance: + { + result["Type"] = "Instance"; + + FileInfo attachment; + if (!db_->LookupAttachment(attachment, id, FileContentType_Dicom)) + { + throw OrthancException(ErrorCode_InternalError); + } - int i; - if (db_->GetMetadataAsInteger(i, id, MetadataType_Instance_IndexInSeries)) - result["IndexInSeries"] = i; - else - result["IndexInSeries"] = Json::nullValue; + result["FileSize"] = static_cast<unsigned int>(attachment.GetUncompressedSize()); + result["FileUuid"] = attachment.GetUuid(); - break; - } + int i; + if (db_->GetMetadataAsInteger(i, id, MetadataType_Instance_IndexInSeries)) + result["IndexInSeries"] = i; + else + result["IndexInSeries"] = Json::nullValue; - default: - throw OrthancException(ErrorCode_InternalError); + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); } // Record the remaining information @@ -666,28 +747,28 @@ switch (currentType) { - case ResourceType_Patient: - patientId = map.GetValue(DICOM_TAG_PATIENT_ID).AsString(); - done = true; - break; + case ResourceType_Patient: + patientId = map.GetValue(DICOM_TAG_PATIENT_ID).AsString(); + done = true; + break; - case ResourceType_Study: - studyInstanceUid = map.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString(); - currentType = ResourceType_Patient; - break; + case ResourceType_Study: + studyInstanceUid = map.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString(); + currentType = ResourceType_Patient; + break; - case ResourceType_Series: - seriesInstanceUid = map.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).AsString(); - currentType = ResourceType_Study; - break; + case ResourceType_Series: + seriesInstanceUid = map.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).AsString(); + currentType = ResourceType_Study; + break; - case ResourceType_Instance: - sopInstanceUid = map.GetValue(DICOM_TAG_SOP_INSTANCE_UID).AsString(); - currentType = ResourceType_Series; - break; + case ResourceType_Instance: + sopInstanceUid = map.GetValue(DICOM_TAG_SOP_INSTANCE_UID).AsString(); + currentType = ResourceType_Series; + break; - default: - throw OrthancException(ErrorCode_InternalError); + default: + throw OrthancException(ErrorCode_InternalError); } // If we have not reached the Patient level, find the parent of @@ -724,4 +805,161 @@ db_->GetLastExportedResource(target); return true; } + + + bool ServerIndex::IsRecyclingNeeded(uint64_t instanceSize) + { + if (maximumStorageSize_ != 0) + { + uint64_t currentSize = currentStorageSize_ - listener_->GetSizeOfFilesToRemove(); + assert(db_->GetTotalCompressedSize() == currentSize); + + if (currentSize + instanceSize > maximumStorageSize_) + { + return true; + } + } + + if (maximumPatients_ != 0) + { + uint64_t patientCount = db_->GetResourceCount(ResourceType_Patient); + if (patientCount > maximumPatients_) + { + return true; + } + } + + return false; + } + + + void ServerIndex::Recycle(uint64_t instanceSize, + const std::string& newPatientId) + { + if (!IsRecyclingNeeded(instanceSize)) + { + return; + } + + // Check whether other DICOM instances from this patient are + // already stored + int64_t patientToAvoid; + ResourceType type; + bool hasPatientToAvoid = db_->LookupResource(newPatientId, patientToAvoid, type); + + if (hasPatientToAvoid && type != ResourceType_Patient) + { + throw OrthancException(ErrorCode_InternalError); + } + + // Iteratively select patient to remove until there is enough + // space in the DICOM store + int64_t patientToRecycle; + while (true) + { + // If other instances of this patient are already in the store, + // we must avoid to recycle them + bool ok = hasPatientToAvoid ? + db_->SelectPatientToRecycle(patientToRecycle, patientToAvoid) : + db_->SelectPatientToRecycle(patientToRecycle); + + if (!ok) + { + throw OrthancException(ErrorCode_FullStorage); + } + + LOG(INFO) << "Recycling one patient"; + db_->DeleteResource(patientToRecycle); + + if (!IsRecyclingNeeded(instanceSize)) + { + // OK, we're done + break; + } + } + } + + void ServerIndex::SetMaximumPatientCount(unsigned int count) + { + boost::mutex::scoped_lock lock(mutex_); + maximumPatients_ = count; + + if (count == 0) + { + LOG(WARNING) << "No limit on the number of stored patients"; + } + else + { + LOG(WARNING) << "At most " << count << " patients will be stored"; + } + + StandaloneRecycling(); + } + + void ServerIndex::SetMaximumStorageSize(uint64_t size) + { + boost::mutex::scoped_lock lock(mutex_); + maximumStorageSize_ = size; + + if (size == 0) + { + LOG(WARNING) << "No limit on the size of the storage area"; + } + else + { + LOG(WARNING) << "At most " << (size / (1024 * 1024)) << "MB will be used for the storage area"; + } + + StandaloneRecycling(); + } + + void ServerIndex::StandaloneRecycling() + { + // WARNING: No mutex here, do not include this as a public method + Transaction t(*this); + Recycle(0, ""); + t.Commit(0); + } + + + bool ServerIndex::IsProtectedPatient(const std::string& publicId) + { + boost::mutex::scoped_lock lock(mutex_); + + // Lookup for the requested resource + int64_t id; + ResourceType type; + if (!db_->LookupResource(publicId, id, type) || + type != ResourceType_Patient) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + return db_->IsProtectedPatient(id); + } + + + void ServerIndex::SetProtectedPatient(const std::string& publicId, + bool isProtected) + { + boost::mutex::scoped_lock lock(mutex_); + + // Lookup for the requested resource + int64_t id; + ResourceType type; + if (!db_->LookupResource(publicId, id, type) || + type != ResourceType_Patient) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + // No need for a SQLite::Transaction here, as we only make 1 write to the DB + db_->SetProtectedPatient(id, isProtected); + + if (isProtected) + LOG(INFO) << "Patient " << publicId << " has been protected"; + else + LOG(INFO) << "Patient " << publicId << " has been unprotected"; + } + }
--- a/OrthancServer/ServerIndex.h Mon Dec 10 11:00:50 2012 +0100 +++ b/OrthancServer/ServerIndex.h Fri Dec 14 11:22:29 2012 +0100 @@ -51,22 +51,33 @@ class ServerIndexListener; } - - class ServerIndex : public boost::noncopyable { private: + class Transaction; + boost::mutex mutex_; boost::thread flushThread_; std::auto_ptr<Internals::ServerIndexListener> listener_; std::auto_ptr<DatabaseWrapper> db_; + uint64_t currentStorageSize_; + uint64_t maximumStorageSize_; + unsigned int maximumPatients_; + void MainDicomTagsToJson(Json::Value& result, int64_t resourceId); SeriesStatus GetSeriesStatus(int id); + bool IsRecyclingNeeded(uint64_t instanceSize); + + void Recycle(uint64_t instanceSize, + const std::string& newPatientId); + + void StandaloneRecycling(); + public: typedef std::list<FileInfo> Attachments; @@ -75,6 +86,22 @@ ~ServerIndex(); + uint64_t GetMaximumStorageSize() const + { + return maximumStorageSize_; + } + + uint64_t GetMaximumPatientCount() const + { + return maximumPatients_; + } + + // "size == 0" means no limit on the storage size + void SetMaximumStorageSize(uint64_t size); + + // "count == 0" means no limit on the number of patients + void SetMaximumPatientCount(unsigned int count); + StoreStatus Store(const DicomMap& dicomSummary, const Attachments& attachments, const std::string& remoteAet); @@ -111,5 +138,9 @@ bool GetLastExportedResource(Json::Value& target); + bool IsProtectedPatient(const std::string& publicId); + + void SetProtectedPatient(const std::string& publicId, + bool isProtected); }; }
--- a/OrthancServer/main.cpp Mon Dec 10 11:00:50 2012 +0100 +++ b/OrthancServer/main.cpp Fri Dec 14 11:22:29 2012 +0100 @@ -214,6 +214,25 @@ ServerContext context(storageDirectory); context.SetCompressionEnabled(GetGlobalBoolParameter("StorageCompression", false)); + try + { + context.GetIndex().SetMaximumPatientCount(GetGlobalIntegerParameter("MaximumPatientCount", 0)); + } + catch (...) + { + context.GetIndex().SetMaximumPatientCount(0); + } + + try + { + uint64_t size = GetGlobalIntegerParameter("MaximumStorageSize", 0); + context.GetIndex().SetMaximumStorageSize(size * 1024 * 1024); + } + catch (...) + { + context.GetIndex().SetMaximumStorageSize(0); + } + MyDicomStoreFactory storeScp(context); {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Archives/MessageWithDestination.cpp Fri Dec 14 11:22:29 2012 +0100 @@ -0,0 +1,171 @@ +#include "../Core/IDynamicObject.h" + +#include "../Core/OrthancException.h" + +#include <stdint.h> +#include <memory> +#include <map> +#include <gtest/gtest.h> +#include <string> +#include <boost/thread.hpp> +#include <boost/date_time/posix_time/posix_time_types.hpp> + +namespace Orthanc +{ + class SharedMessageQueue + { + private: + typedef std::list<IDynamicObject*> Queue; + + unsigned int maxSize_; + Queue queue_; + boost::mutex mutex_; + boost::condition_variable elementAvailable_; + + public: + SharedMessageQueue(unsigned int maxSize = 0) + { + maxSize_ = maxSize; + } + + ~SharedMessageQueue() + { + for (Queue::iterator it = queue_.begin(); it != queue_.end(); it++) + { + delete *it; + } + } + + void Enqueue(IDynamicObject* message) + { + boost::mutex::scoped_lock lock(mutex_); + + if (maxSize_ != 0 && queue_.size() > maxSize_) + { + // Too many elements in the queue: First remove the oldest + delete queue_.front(); + queue_.pop_front(); + } + + queue_.push_back(message); + elementAvailable_.notify_one(); + } + + IDynamicObject* Dequeue(int32_t timeout) + { + boost::mutex::scoped_lock lock(mutex_); + + // Wait for a message to arrive in the FIFO queue + while (queue_.empty()) + { + if (timeout == 0) + { + elementAvailable_.wait(lock); + } + else + { + bool success = elementAvailable_.timed_wait + (lock, boost::posix_time::milliseconds(timeout)); + if (!success) + { + throw OrthancException(ErrorCode_Timeout); + } + } + } + + std::auto_ptr<IDynamicObject> message(queue_.front()); + queue_.pop_front(); + + return message.release(); + } + + IDynamicObject* Dequeue() + { + return Dequeue(0); + } + }; + + + /** + * This class represents a message that is to be sent to some destination. + **/ + class MessageToDispatch : public boost::noncopyable + { + private: + IDynamicObject* message_; + std::string destination_; + + public: + /** + * Create a new message with a destination. + * \param message The content of the message (takes the ownership) + * \param destination The destination of the message + **/ + MessageToDispatch(IDynamicObject* message, + const char* destination) + { + message_ = message; + destination_ = destination; + } + + ~MessageToDispatch() + { + if (message_) + { + delete message_; + } + } + }; + + + class IDestinationContext : public IDynamicObject + { + public: + virtual void Handle(const IDynamicObject& message) = 0; + }; + + + class IDestinationContextFactory : public IDynamicObject + { + public: + virtual IDestinationContext* Construct(const char* destination) = 0; + }; + + + class MessageDispatcher + { + private: + typedef std::map<std::string, IDestinationContext*> ActiveContexts; + + std::auto_ptr<IDestinationContextFactory> factory_; + ActiveContexts activeContexts_; + SharedMessageQueue queue_; + + public: + MessageDispatcher(IDestinationContextFactory* factory) // takes the ownership + { + factory_.reset(factory); + } + + ~MessageDispatcher() + { + for (ActiveContexts::iterator it = activeContexts_.begin(); + it != activeContexts_.end(); it++) + { + delete it->second; + } + } + }; +} + + + +#include "../Core/DicomFormat/DicomString.h" + +using namespace Orthanc; + +TEST(MessageToDispatch, A) +{ + MessageToDispatch a(new DicomString("coucou"), "pukkaj"); +} +
--- a/Resources/CMake/DcmtkConfiguration.cmake Mon Dec 10 11:00:50 2012 +0100 +++ b/Resources/CMake/DcmtkConfiguration.cmake Fri Dec 14 11:22:29 2012 +0100 @@ -71,14 +71,14 @@ set(DCMTK_BUNDLES_LOG4CPLUS 1) else() - #include(FindDCMTK) - set(DCMTK_DIR /usr/include/dcmtk) - set(DCMTK_INCLUDE_DIR ${DCMTK_DIR}) - - #message(${DCMTK_LIBRARIES}) + # The following line allows to manually add libraries at the + # command-line, which is necessary for Ubuntu/Debian packages + set(tmp "${DCMTK_LIBRARIES}") + include(FindDCMTK) + list(APPEND DCMTK_LIBRARIES "${tmp}") include_directories(${DCMTK_INCLUDE_DIR}) - link_libraries(dcmdata dcmnet wrap ofstd) + link_libraries(${DCMTK_LIBRARIES}) add_definitions( -DHAVE_CONFIG_H=1 @@ -93,17 +93,17 @@ endif() # Autodetection of the version of DCMTK - file(STRINGS "${DCMTK_CONFIGURATION_FILE}" DCMTK_VERSION_NUMBER1 REGEX ".*PACKAGE_VERSION .*") - string(REGEX REPLACE ".*PACKAGE_VERSION.*\"([0-9]*)\\.([0-9]*)\\.([0-9]*)\"$" "\\1\\2\\3" DCMTK_VERSION_NUMBER ${DCMTK_VERSION_NUMBER1}) + file(STRINGS + "${DCMTK_CONFIGURATION_FILE}" + DCMTK_VERSION_NUMBER1 REGEX + ".*PACKAGE_VERSION .*") - IF (EXISTS "${DCMTK_DIR}/oflog") - set(DCMTK_BUNDLES_LOG4CPLUS 1) - link_libraries(oflog) - else() - set(DCMTK_BUNDLES_LOG4CPLUS 0) - endif() + string(REGEX REPLACE + ".*PACKAGE_VERSION.*\"([0-9]*)\\.([0-9]*)\\.([0-9]*)\"$" + "\\1\\2\\3" + DCMTK_VERSION_NUMBER + ${DCMTK_VERSION_NUMBER1}) endif() add_definitions(-DDCMTK_VERSION_NUMBER=${DCMTK_VERSION_NUMBER}) message("DCMTK version: ${DCMTK_VERSION_NUMBER}") -message("Does DCMTK includes its own copy of Log4Cplus: ${DCMTK_BUNDLES_LOG4CPLUS}")
--- a/Resources/Configuration.json Mon Dec 10 11:00:50 2012 +0100 +++ b/Resources/Configuration.json Fri Dec 14 11:22:29 2012 +0100 @@ -13,6 +13,14 @@ // Enable the transparent compression of the DICOM instances "StorageCompression" : false, + // Maximum size of the storage in MB (a value of "0" indicates no + // limit on the storage size) + "MaximumStorageSize" : 0, + + // Maximum number of patients that can be stored at a given time + // in the storage (a value of "0" indicates no limit on the number + // of patients) + "MaximumPatientCount" : 0, /**
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Samples/ImportDicomFiles/ImportDicomFiles.py Fri Dec 14 11:22:29 2012 +0100 @@ -0,0 +1,61 @@ +#!/usr/bin/python + +import os +import sys +import os.path +import httplib2 + +if len(sys.argv) != 4: + print(""" +Sample script to recursively import in Orthanc all the DICOM files +that are stored in some path. Please make sure that Orthanc is running +before starting this script. The files are uploaded through the REST +API. + +Usage: %s [hostname] [HTTP port] [path] +For instance: %s localhost 8042 . +""" % (sys.argv[0], sys.argv[0])) + exit(-1) + +URL = 'http://%s:%d/instances' % (sys.argv[1], int(sys.argv[2])) + +success = 0 + + +# This function will upload a single file to Orthanc through the REST API +def UploadFile(path): + global success + + f = open(path, "r") + content = f.read() + f.close() + + try: + sys.stdout.write("Importing %s" % path) + + h = httplib2.Http() + resp, content = h.request(URL, 'POST', + body = content, + headers = { 'content-type' : 'application/dicom' }) + + if resp.status == 200: + sys.stdout.write(" => success\n") + success += 1 + else: + sys.stdout.write(" => failure (is it a DICOM file?)\n") + + except: + sys.stdout.write(" => unable to connect\n") + + +if os.path.isfile(sys.argv[3]): + # Upload a single file + UploadFile(sys.argv[3]) +else: + # Recursively upload a directory + for root, dirs, files in os.walk(sys.argv[3]): + for f in files: + UploadFile(os.path.join(root, f)) + + +print("\nSummary: %d DICOM file(s) have been imported" % success)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Samples/RestApi/CMakeLists.txt Fri Dec 14 11:22:29 2012 +0100 @@ -0,0 +1,62 @@ +cmake_minimum_required(VERSION 2.8) + +project(RestApiSample) + +include(ExternalProject) + +# Send the toolchain information to the Orthanc +if (CMAKE_TOOLCHAIN_FILE) + set(TOOLCHAIN "-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}") +endif() + +ExternalProject_Add( + ORTHANC_CORE + + # We use the Orthanc-0.3.1 branch for this sample + DOWNLOAD_COMMAND hg clone https://code.google.com/p/orthanc/ -r Orthanc-0.3.1 + + # Optional step, to reuse the third-party downloads + PATCH_COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_SOURCE_DIR}/../../../ThirdPartyDownloads ThirdPartyDownloads + + PREFIX ${CMAKE_BINARY_DIR}/Orthanc/ + UPDATE_COMMAND "" + SOURCE_DIR ${CMAKE_BINARY_DIR}/Orthanc/src/orthanc/ + CMAKE_COMMAND ${CMAKE_COMMAND} + CMAKE_ARGS -DSTATIC_BUILD=ON -DSTANDALONE_BUILD=ON -DUSE_DYNAMIC_GOOGLE_LOG=OFF -DUSE_DYNAMIC_SQLITE=OFF -DONLY_CORE_LIBRARY=ON -DENABLE_SSL=OFF ${TOOLCHAIN} + BUILD_COMMAND $(MAKE) + INSTALL_COMMAND "" + BUILD_IN_SOURCE 0 + ) + +ExternalProject_Get_Property(ORTHANC_CORE source_dir) +include_directories(${source_dir}) + +ExternalProject_Get_Property(ORTHANC_CORE binary_dir) +link_directories(${binary_dir}) +include_directories(${binary_dir}/jsoncpp-src-0.5.0/include) +include_directories(${binary_dir}/glog-0.3.2/src) +include_directories(${binary_dir}/boost_1_49_0) + + +add_executable(RestApiSample + Sample.cpp + ) + +add_dependencies(RestApiSample ORTHANC_CORE) + +target_link_libraries(RestApiSample + # From Orthanc + CoreLibrary + GoogleLog + + # These two libraries are not necessary + #OpenSSL + #Curl + ) + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + target_link_libraries(RestApiSample pthread) +elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + add_definitions(-DGOOGLE_GLOG_DLL_DECL=) + target_link_libraries(RestApiSample wsock32) +endif()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Samples/RestApi/Sample.cpp Fri Dec 14 11:22:29 2012 +0100 @@ -0,0 +1,105 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012 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 <http://www.gnu.org/licenses/>. + **/ + + +#include <Core/HttpServer/MongooseServer.h> +#include <Core/RestApi/RestApi.h> +#include <Core/Toolbox.h> +#include <glog/logging.h> +#include <stdio.h> + + +/** + * This is a demo program that shows how to setup a REST server with + * the Orthanc Core API. Once the server is running, here are some + * sample command lines to interact with it: + * + * # curl http://localhost:8042 + * # curl 'http://localhost:8042?name=Hide' + * # curl http://localhost:8042 -X DELETE + * # curl http://localhost:8042 -X PUT -d "PutBody" + * # curl http://localhost:8042 -X POST -d "PostBody" + **/ + +static void GetRoot(Orthanc::RestApi::GetCall& call) +{ + std::string answer = "Hello world\n"; + answer += "Glad to meet you, Mr. " + call.GetArgument("name", "Nobody") + "\n"; + call.GetOutput().AnswerBuffer(answer, "text/plain"); +} + +static void DeleteRoot(Orthanc::RestApi::DeleteCall& call) +{ + call.GetOutput().AnswerBuffer("Hey, you have just deleted the server!\n", + "text/plain"); +} + +static void PostRoot(Orthanc::RestApi::PostCall& call) +{ + call.GetOutput().AnswerBuffer("I have received a POST with body: [" + + call.GetPostBody() + "]\n", "text/plain"); +} + +static void PutRoot(Orthanc::RestApi::PutCall& call) +{ + call.GetOutput().AnswerBuffer("I have received a PUT with body: [" + + call.GetPutBody() + "]\n", "text/plain"); +} + +int main() +{ + // Initialize the logging mechanism + google::InitGoogleLogging("Orthanc"); + FLAGS_logtostderr = true; + FLAGS_minloglevel = 0; // Use the verbose mode + FLAGS_v = 0; + + // Define the callbacks of the REST API + std::auto_ptr<Orthanc::RestApi> rest(new Orthanc::RestApi); + rest->Register("/", GetRoot); + rest->Register("/", PostRoot); + rest->Register("/", PutRoot); + rest->Register("/", DeleteRoot); + + // Setup the embedded HTTP server + Orthanc::MongooseServer httpServer; + httpServer.SetPortNumber(8042); // Use TCP port 8042 + httpServer.SetRemoteAccessAllowed(true); // Do not block remote requests + httpServer.RegisterHandler(rest.release()); // The REST API is the handler + + // Start the server and wait for the user to hit "Ctrl-C" + httpServer.Start(); + LOG(WARNING) << "REST server has started"; + Orthanc::Toolbox::ServerBarrier(); + LOG(WARNING) << "REST server has stopped"; + + return 0; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/UnitTests/MemoryCache.cpp Fri Dec 14 11:22:29 2012 +0100 @@ -0,0 +1,128 @@ +#include "gtest/gtest.h" + +#include <glog/logging.h> +#include <memory> +#include <boost/thread.hpp> +#include <boost/lexical_cast.hpp> +#include "../Core/IDynamicObject.h" +#include "../Core/Cache/MemoryCache.h" + + +TEST(CacheIndex, Basic) +{ + Orthanc::CacheIndex<std::string> r; + + r.Add("d"); + r.Add("a"); + r.Add("c"); + r.Add("b"); + + r.TagAsMostRecent("a"); + r.TagAsMostRecent("d"); + r.TagAsMostRecent("b"); + r.TagAsMostRecent("c"); + r.TagAsMostRecent("d"); + r.TagAsMostRecent("c"); + + ASSERT_EQ("a", r.RemoveOldest()); + ASSERT_EQ("b", r.RemoveOldest()); + ASSERT_EQ("d", r.RemoveOldest()); + ASSERT_EQ("c", r.RemoveOldest()); + + ASSERT_TRUE(r.IsEmpty()); +} + + +TEST(CacheIndex, Payload) +{ + Orthanc::CacheIndex<std::string, int> r; + + r.Add("a", 420); + r.Add("b", 421); + r.Add("c", 422); + r.Add("d", 423); + + r.TagAsMostRecent("a"); + r.TagAsMostRecent("d"); + r.TagAsMostRecent("b"); + r.TagAsMostRecent("c"); + r.TagAsMostRecent("d"); + r.TagAsMostRecent("c"); + + ASSERT_TRUE(r.Contains("b")); + ASSERT_EQ(421, r.Invalidate("b")); + ASSERT_FALSE(r.Contains("b")); + + int p; + ASSERT_TRUE(r.Contains("a", p)); ASSERT_EQ(420, p); + ASSERT_TRUE(r.Contains("c", p)); ASSERT_EQ(422, p); + ASSERT_TRUE(r.Contains("d", p)); ASSERT_EQ(423, p); + + ASSERT_EQ("a", r.RemoveOldest(p)); ASSERT_EQ(420, p); + ASSERT_EQ("d", r.RemoveOldest(p)); ASSERT_EQ(423, p); + ASSERT_EQ("c", r.RemoveOldest(p)); ASSERT_EQ(422, p); + + ASSERT_TRUE(r.IsEmpty()); +} + + + + +namespace +{ + class Integer : public Orthanc::IDynamicObject + { + private: + std::string& log_; + int value_; + + public: + Integer(std::string& log, int v) : log_(log), value_(v) + { + } + + virtual ~Integer() + { + LOG(INFO) << "Removing cache entry for " << value_; + log_ += boost::lexical_cast<std::string>(value_) + " "; + } + + int GetValue() const + { + return value_; + } + }; + + class IntegerProvider : public Orthanc::ICachePageProvider + { + public: + std::string log_; + + Orthanc::IDynamicObject* Provide(const std::string& s) + { + LOG(INFO) << "Providing " << s; + return new Integer(log_, boost::lexical_cast<int>(s)); + } + }; +} + + +TEST(MemoryCache, Basic) +{ + IntegerProvider provider; + + { + Orthanc::MemoryCache cache(provider, 3); + cache.Access("42"); // 42 -> exit + cache.Access("43"); // 43, 42 -> exit + cache.Access("45"); // 45, 43, 42 -> exit + cache.Access("42"); // 42, 45, 43 -> exit + cache.Access("43"); // 43, 42, 45 -> exit + cache.Access("47"); // 45 is removed; 47, 43, 42 -> exit + cache.Access("44"); // 42 is removed; 44, 47, 43 -> exit + cache.Access("42"); // 43 is removed; 42, 44, 47 -> exit + // Closing the cache: 47, 44, 42 are successively removed + } + + ASSERT_EQ("45 42 43 47 44 42 ", provider.log_); +}
--- a/UnitTests/MessageWithDestination.cpp Mon Dec 10 11:00:50 2012 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,171 +0,0 @@ -#include "../Core/IDynamicObject.h" - -#include "../Core/OrthancException.h" - -#include <stdint.h> -#include <memory> -#include <map> -#include <gtest/gtest.h> -#include <string> -#include <boost/thread.hpp> -#include <boost/date_time/posix_time/posix_time_types.hpp> - -namespace Orthanc -{ - class SharedMessageQueue - { - private: - typedef std::list<IDynamicObject*> Queue; - - unsigned int maxSize_; - Queue queue_; - boost::mutex mutex_; - boost::condition_variable elementAvailable_; - - public: - SharedMessageQueue(unsigned int maxSize = 0) - { - maxSize_ = maxSize; - } - - ~SharedMessageQueue() - { - for (Queue::iterator it = queue_.begin(); it != queue_.end(); it++) - { - delete *it; - } - } - - void Enqueue(IDynamicObject* message) - { - boost::mutex::scoped_lock lock(mutex_); - - if (maxSize_ != 0 && queue_.size() > maxSize_) - { - // Too many elements in the queue: First remove the oldest - delete queue_.front(); - queue_.pop_front(); - } - - queue_.push_back(message); - elementAvailable_.notify_one(); - } - - IDynamicObject* Dequeue(int32_t timeout) - { - boost::mutex::scoped_lock lock(mutex_); - - // Wait for a message to arrive in the FIFO queue - while (queue_.empty()) - { - if (timeout == 0) - { - elementAvailable_.wait(lock); - } - else - { - bool success = elementAvailable_.timed_wait - (lock, boost::posix_time::milliseconds(timeout)); - if (!success) - { - throw OrthancException(ErrorCode_Timeout); - } - } - } - - std::auto_ptr<IDynamicObject> message(queue_.front()); - queue_.pop_front(); - - return message.release(); - } - - IDynamicObject* Dequeue() - { - return Dequeue(0); - } - }; - - - /** - * This class represents a message that is to be sent to some destination. - **/ - class MessageToDispatch : public boost::noncopyable - { - private: - IDynamicObject* message_; - std::string destination_; - - public: - /** - * Create a new message with a destination. - * \param message The content of the message (takes the ownership) - * \param destination The destination of the message - **/ - MessageToDispatch(IDynamicObject* message, - const char* destination) - { - message_ = message; - destination_ = destination; - } - - ~MessageToDispatch() - { - if (message_) - { - delete message_; - } - } - }; - - - class IDestinationContext : public IDynamicObject - { - public: - virtual void Handle(const IDynamicObject& message) = 0; - }; - - - class IDestinationContextFactory : public IDynamicObject - { - public: - virtual IDestinationContext* Construct(const char* destination) = 0; - }; - - - class MessageDispatcher - { - private: - typedef std::map<std::string, IDestinationContext*> ActiveContexts; - - std::auto_ptr<IDestinationContextFactory> factory_; - ActiveContexts activeContexts_; - SharedMessageQueue queue_; - - public: - MessageDispatcher(IDestinationContextFactory* factory) // takes the ownership - { - factory_.reset(factory); - } - - ~MessageDispatcher() - { - for (ActiveContexts::iterator it = activeContexts_.begin(); - it != activeContexts_.end(); it++) - { - delete it->second; - } - } - }; -} - - - -#include "../Core/DicomFormat/DicomString.h" - -using namespace Orthanc; - -TEST(MessageToDispatch, A) -{ - MessageToDispatch a(new DicomString("coucou"), "pukkaj"); -} -
--- a/UnitTests/RestApi.cpp Mon Dec 10 11:00:50 2012 +0100 +++ b/UnitTests/RestApi.cpp Fri Dec 14 11:22:29 2012 +0100 @@ -50,41 +50,3 @@ ASSERT_EQ("c", trail[2]); } } - - - -#if 0 - -#include "../Core/HttpServer/MongooseServer.h" - -struct Tutu : public IDynamicObject -{ - static void Toto(RestApi::GetCall& call) - { - printf("DONE\n"); - Json::Value a = Json::objectValue; - a["Tutu"] = "Toto"; - a["Youpie"] = call.GetArgument("coucou", "nope"); - a["Toto"] = call.GetUriComponent("test", "nope"); - call.GetOutput().AnswerJson(a); - } -}; - - - -TEST(RestApi, Tutu) -{ - MongooseServer httpServer; - httpServer.SetPortNumber(8042); - httpServer.Start(); - - RestApi* api = new RestApi; - httpServer.RegisterHandler(api); - api->Register("/coucou/{test}/a/*", Tutu::Toto); - - httpServer.Start(); - /*LOG(WARNING) << "REST has started"; - Toolbox::ServerBarrier();*/ -} - -#endif
--- a/UnitTests/ServerIndex.cpp Mon Dec 10 11:00:50 2012 +0100 +++ b/UnitTests/ServerIndex.cpp Fri Dec 14 11:22:29 2012 +0100 @@ -1,6 +1,7 @@ #include "gtest/gtest.h" #include "../OrthancServer/DatabaseWrapper.h" +#include "../Core/Uuid.h" #include <ctype.h> #include <glog/logging.h> @@ -12,7 +13,7 @@ class ServerIndexListener : public IServerIndexListener { public: - std::set<std::string> deletedFiles_; + std::vector<std::string> deletedFiles_; std::string ancestorId_; ResourceType ancestorType_; @@ -29,9 +30,10 @@ ancestorType_ = type; } - virtual void SignalFileDeleted(const std::string& fileUuid) + virtual void SignalFileDeleted(const FileInfo& info) { - deletedFiles_.insert(fileUuid); + const std::string fileUuid = info.GetUuid(); + deletedFiles_.push_back(fileUuid); LOG(INFO) << "A file must be removed: " << fileUuid; } }; @@ -170,8 +172,12 @@ index.DeleteResource(a[0]); ASSERT_EQ(2u, listener.deletedFiles_.size()); - ASSERT_FALSE(listener.deletedFiles_.find("my json file") == listener.deletedFiles_.end()); - ASSERT_FALSE(listener.deletedFiles_.find("my dicom file") == listener.deletedFiles_.end()); + ASSERT_FALSE(std::find(listener.deletedFiles_.begin(), + listener.deletedFiles_.end(), + "my json file") == listener.deletedFiles_.end()); + ASSERT_FALSE(std::find(listener.deletedFiles_.begin(), + listener.deletedFiles_.end(), + "my dicom file") == listener.deletedFiles_.end()); ASSERT_EQ(2u, index.GetTableRecordCount("Resources")); ASSERT_EQ(0u, index.GetTableRecordCount("Metadata")); @@ -183,7 +189,9 @@ ASSERT_EQ(2u, index.GetTableRecordCount("GlobalProperties")); ASSERT_EQ(3u, listener.deletedFiles_.size()); - ASSERT_FALSE(listener.deletedFiles_.find("world") == listener.deletedFiles_.end()); + ASSERT_FALSE(std::find(listener.deletedFiles_.begin(), + listener.deletedFiles_.end(), + "world") == listener.deletedFiles_.end()); } @@ -256,3 +264,135 @@ index.DeleteResource(a[6]); ASSERT_EQ("", listener.ancestorId_); // No more ancestor } + + +TEST(DatabaseWrapper, PatientRecycling) +{ + ServerIndexListener listener; + DatabaseWrapper index(listener); + + std::vector<int64_t> patients; + for (int i = 0; i < 10; i++) + { + std::string p = "Patient " + boost::lexical_cast<std::string>(i); + patients.push_back(index.CreateResource(p, ResourceType_Patient)); + index.AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10)); + ASSERT_FALSE(index.IsProtectedPatient(patients[i])); + } + + ASSERT_EQ(10u, index.GetTableRecordCount("Resources")); + ASSERT_EQ(10u, index.GetTableRecordCount("PatientRecyclingOrder")); + + listener.Reset(); + + index.DeleteResource(patients[5]); + index.DeleteResource(patients[0]); + ASSERT_EQ(8u, index.GetTableRecordCount("Resources")); + ASSERT_EQ(8u, index.GetTableRecordCount("PatientRecyclingOrder")); + + ASSERT_EQ(2u, listener.deletedFiles_.size()); + ASSERT_EQ("Patient 5", listener.deletedFiles_[0]); + ASSERT_EQ("Patient 0", listener.deletedFiles_[1]); + + int64_t p; + ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[1]); + index.DeleteResource(p); + ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]); + index.DeleteResource(p); + ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[3]); + index.DeleteResource(p); + ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]); + index.DeleteResource(p); + ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[6]); + index.DeleteResource(p); + index.DeleteResource(patients[8]); + ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[7]); + index.DeleteResource(p); + ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[9]); + index.DeleteResource(p); + ASSERT_FALSE(index.SelectPatientToRecycle(p)); + + ASSERT_EQ(10u, listener.deletedFiles_.size()); + ASSERT_EQ(0u, index.GetTableRecordCount("Resources")); + ASSERT_EQ(0u, index.GetTableRecordCount("PatientRecyclingOrder")); +} + + +TEST(DatabaseWrapper, PatientProtection) +{ + ServerIndexListener listener; + DatabaseWrapper index(listener); + + std::vector<int64_t> patients; + for (int i = 0; i < 5; i++) + { + std::string p = "Patient " + boost::lexical_cast<std::string>(i); + patients.push_back(index.CreateResource(p, ResourceType_Patient)); + index.AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10)); + ASSERT_FALSE(index.IsProtectedPatient(patients[i])); + } + + ASSERT_EQ(5u, index.GetTableRecordCount("Resources")); + ASSERT_EQ(5u, index.GetTableRecordCount("PatientRecyclingOrder")); + + ASSERT_FALSE(index.IsProtectedPatient(patients[2])); + index.SetProtectedPatient(patients[2], true); + ASSERT_TRUE(index.IsProtectedPatient(patients[2])); + ASSERT_EQ(4u, index.GetTableRecordCount("PatientRecyclingOrder")); + ASSERT_EQ(5u, index.GetTableRecordCount("Resources")); + + index.SetProtectedPatient(patients[2], true); + ASSERT_TRUE(index.IsProtectedPatient(patients[2])); + ASSERT_EQ(4u, index.GetTableRecordCount("PatientRecyclingOrder")); + index.SetProtectedPatient(patients[2], false); + ASSERT_FALSE(index.IsProtectedPatient(patients[2])); + ASSERT_EQ(5u, index.GetTableRecordCount("PatientRecyclingOrder")); + index.SetProtectedPatient(patients[2], false); + ASSERT_FALSE(index.IsProtectedPatient(patients[2])); + ASSERT_EQ(5u, index.GetTableRecordCount("PatientRecyclingOrder")); + + ASSERT_EQ(5u, index.GetTableRecordCount("Resources")); + ASSERT_EQ(5u, index.GetTableRecordCount("PatientRecyclingOrder")); + index.SetProtectedPatient(patients[2], true); + ASSERT_TRUE(index.IsProtectedPatient(patients[2])); + ASSERT_EQ(4u, index.GetTableRecordCount("PatientRecyclingOrder")); + index.SetProtectedPatient(patients[2], false); + ASSERT_FALSE(index.IsProtectedPatient(patients[2])); + ASSERT_EQ(5u, index.GetTableRecordCount("PatientRecyclingOrder")); + index.SetProtectedPatient(patients[3], true); + ASSERT_TRUE(index.IsProtectedPatient(patients[3])); + ASSERT_EQ(4u, index.GetTableRecordCount("PatientRecyclingOrder")); + + ASSERT_EQ(5u, index.GetTableRecordCount("Resources")); + ASSERT_EQ(0u, listener.deletedFiles_.size()); + + // Unprotecting a patient puts it at the last position in the recycling queue + int64_t p; + ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[0]); + index.DeleteResource(p); + ASSERT_TRUE(index.SelectPatientToRecycle(p, patients[1])); ASSERT_EQ(p, patients[4]); + ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[1]); + index.DeleteResource(p); + ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]); + index.DeleteResource(p); + ASSERT_FALSE(index.SelectPatientToRecycle(p, patients[2])); + ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]); + index.DeleteResource(p); + // "patients[3]" is still protected + ASSERT_FALSE(index.SelectPatientToRecycle(p)); + + ASSERT_EQ(4u, listener.deletedFiles_.size()); + ASSERT_EQ(1u, index.GetTableRecordCount("Resources")); + ASSERT_EQ(0u, index.GetTableRecordCount("PatientRecyclingOrder")); + + index.SetProtectedPatient(patients[3], false); + ASSERT_EQ(1u, index.GetTableRecordCount("PatientRecyclingOrder")); + ASSERT_FALSE(index.SelectPatientToRecycle(p, patients[3])); + ASSERT_TRUE(index.SelectPatientToRecycle(p, patients[2])); + ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[3]); + index.DeleteResource(p); + + ASSERT_EQ(5u, listener.deletedFiles_.size()); + ASSERT_EQ(0u, index.GetTableRecordCount("Resources")); + ASSERT_EQ(0u, index.GetTableRecordCount("PatientRecyclingOrder")); +}