# HG changeset patch # User Sebastien Jodogne # Date 1379064308 -7200 # Node ID 0e510ea3de31a5778c91c38e2d1a33b01229e529 # Parent fe796b0538635d27159f0f9af625a86ebf6541ae# Parent c0a841f282a95ff5369e1c244ef3abc3d4fada7b merge mainline -> laaw diff -r fe796b053863 -r 0e510ea3de31 CMakeLists.txt --- a/CMakeLists.txt Fri Sep 13 11:10:58 2013 +0200 +++ b/CMakeLists.txt Fri Sep 13 11:25:08 2013 +0200 @@ -20,24 +20,20 @@ SET(BUILD_CLIENT_LIBRARY ON CACHE BOOL "Build the client library") SET(DCMTK_DICTIONARY_DIR "/usr/share/dcmtk" CACHE PATH "Directory containing the DCMTK dictionaries \"dicom.dic\" and \"private.dic\" (ignored in standalone builds)") -# Advanced parameters (for Debian packaging) -SET(USE_DYNAMIC_JSONCPP OFF CACHE BOOL "Use the dynamic version of JsonCpp (only for Debian sid)") +# Advanced parameters to fine-tune linking against system libraries +SET(USE_DYNAMIC_JSONCPP OFF CACHE BOOL "Use the dynamic version of JsonCpp") SET(USE_DYNAMIC_GOOGLE_LOG ON CACHE BOOL "Use the dynamic version of Google Log") -SET(USE_DYNAMIC_GOOGLE_TEST ON CACHE BOOL "Use the dynamic version of Google Test (not for Debian sid)") +SET(USE_DYNAMIC_GOOGLE_TEST ON CACHE BOOL "Use the dynamic version of Google Test") SET(USE_DYNAMIC_SQLITE ON CACHE BOOL "Use the dynamic version of SQLite") SET(USE_DYNAMIC_MONGOOSE OFF CACHE BOOL "Use the dynamic version of Mongoose") SET(USE_DYNAMIC_LUA OFF CACHE BOOL "Use the dynamic version of Lua") -SET(DEBIAN_FORCE_HARDENING OFF CACHE BOOL "Force the injection of Debian hardening flags (unrecommended)") -SET(DEBIAN_USE_GTEST_SOURCE_PACKAGE OFF CACHE BOOL "Use the sources of Google Test shipped with libgtest-dev (only for Debian sid)") -SET(ONLY_CORE_LIBRARY OFF CACHE BOOL "Only build the core library") +SET(DEBIAN_USE_GTEST_SOURCE_PACKAGE OFF CACHE BOOL "Use the sources of Google Test shipped with libgtest-dev (Debian only)") mark_as_advanced(USE_DYNAMIC_JSONCPP) mark_as_advanced(USE_DYNAMIC_GOOGLE_LOG) mark_as_advanced(USE_DYNAMIC_GOOGLE_TEST) mark_as_advanced(USE_DYNAMIC_SQLITE) -mark_as_advanced(DEBIAN_FORCE_HARDENING) -mark_as_advanced(DEBIAN_USE_STATIC_GOOGLE_TEST) -mark_as_advanced(ONLY_CORE_LIBRARY) +mark_as_advanced(DEBIAN_USE_GTEST_SOURCE_PACKAGE) # Some basic inclusions include(CheckIncludeFiles) @@ -79,11 +75,7 @@ endif() include(${CMAKE_SOURCE_DIR}/Resources/CMake/BoostConfiguration.cmake) - -if(NOT ONLY_CORE_LIBRARY) - include(${CMAKE_SOURCE_DIR}/Resources/CMake/DcmtkConfiguration.cmake) -endif() - +include(${CMAKE_SOURCE_DIR}/Resources/CMake/DcmtkConfiguration.cmake) include(${CMAKE_SOURCE_DIR}/Resources/CMake/MongooseConfiguration.cmake) include(${CMAKE_SOURCE_DIR}/Resources/CMake/ZlibConfiguration.cmake) include(${CMAKE_SOURCE_DIR}/Resources/CMake/SQLiteConfiguration.cmake) @@ -193,83 +185,81 @@ ) +##################################################################### +## Build the Orthanc server +##################################################################### -if(NOT ONLY_CORE_LIBRARY) - ##################################################################### - ## Build the Orthanc server - ##################################################################### +add_library(ServerLibrary + STATIC + ${DCMTK_SOURCES} + OrthancServer/DicomProtocol/DicomFindAnswers.cpp + OrthancServer/DicomProtocol/DicomServer.cpp + OrthancServer/DicomProtocol/DicomUserConnection.cpp + OrthancServer/FromDcmtkBridge.cpp + OrthancServer/Internals/CommandDispatcher.cpp + OrthancServer/Internals/FindScp.cpp + OrthancServer/Internals/MoveScp.cpp + OrthancServer/Internals/StoreScp.cpp + OrthancServer/OrthancInitialization.cpp + OrthancServer/OrthancRestApi.cpp + OrthancServer/ServerIndex.cpp + OrthancServer/ToDcmtkBridge.cpp + OrthancServer/DatabaseWrapper.cpp + OrthancServer/ServerContext.cpp + OrthancServer/ServerEnumerations.cpp + OrthancServer/ServerToolbox.cpp + ) + +# Ensure autogenerated code is built before building ServerLibrary +add_dependencies(ServerLibrary CoreLibrary) + +add_executable(Orthanc + OrthancServer/main.cpp + ) + +target_link_libraries(Orthanc ServerLibrary CoreLibrary) - add_library(ServerLibrary - STATIC - ${DCMTK_SOURCES} - OrthancServer/DicomProtocol/DicomFindAnswers.cpp - OrthancServer/DicomProtocol/DicomServer.cpp - OrthancServer/DicomProtocol/DicomUserConnection.cpp - OrthancServer/FromDcmtkBridge.cpp - OrthancServer/Internals/CommandDispatcher.cpp - OrthancServer/Internals/FindScp.cpp - OrthancServer/Internals/MoveScp.cpp - OrthancServer/Internals/StoreScp.cpp - OrthancServer/OrthancInitialization.cpp - OrthancServer/OrthancRestApi.cpp - OrthancServer/ServerIndex.cpp - OrthancServer/ToDcmtkBridge.cpp - OrthancServer/DatabaseWrapper.cpp - OrthancServer/ServerContext.cpp - OrthancServer/ServerEnumerations.cpp - OrthancServer/ServerToolbox.cpp +if (${OPENSSL_SOURCES_LENGTH} GREATER 0) + target_link_libraries(Orthanc OpenSSL) +endif() + +install( + TARGETS Orthanc + RUNTIME DESTINATION bin + ) + + + +##################################################################### +## Build the unit tests if required +##################################################################### + +if (BUILD_UNIT_TESTS) + add_definitions(-DORTHANC_BUILD_UNIT_TESTS=1) + include(${CMAKE_SOURCE_DIR}/Resources/CMake/GoogleTestConfiguration.cmake) + add_executable(UnitTests + ${GTEST_SOURCES} + UnitTests/FileStorage.cpp + UnitTests/MemoryCache.cpp + UnitTests/Png.cpp + UnitTests/RestApi.cpp + UnitTests/SQLite.cpp + UnitTests/SQLiteChromium.cpp + UnitTests/ServerIndex.cpp + UnitTests/Versions.cpp + UnitTests/Zip.cpp + UnitTests/Lua.cpp + UnitTests/main.cpp ) - - # Ensure autogenerated code is built before building ServerLibrary - add_dependencies(ServerLibrary CoreLibrary) - - add_executable(Orthanc - OrthancServer/main.cpp - ) - - target_link_libraries(Orthanc ServerLibrary CoreLibrary) + target_link_libraries(UnitTests ServerLibrary CoreLibrary) if (${OPENSSL_SOURCES_LENGTH} GREATER 0) - target_link_libraries(Orthanc OpenSSL) - endif() - - install( - TARGETS Orthanc - RUNTIME DESTINATION bin - ) - - - ##################################################################### - ## Build the unit tests if required - ##################################################################### - - if (BUILD_UNIT_TESTS) - add_definitions(-DORTHANC_BUILD_UNIT_TESTS=1) - include(${CMAKE_SOURCE_DIR}/Resources/CMake/GoogleTestConfiguration.cmake) - add_executable(UnitTests - ${GTEST_SOURCES} - UnitTests/FileStorage.cpp - UnitTests/MemoryCache.cpp - UnitTests/Png.cpp - UnitTests/RestApi.cpp - UnitTests/SQLite.cpp - UnitTests/SQLiteChromium.cpp - UnitTests/ServerIndex.cpp - UnitTests/Versions.cpp - UnitTests/Zip.cpp - UnitTests/Lua.cpp - UnitTests/main.cpp - ) - target_link_libraries(UnitTests ServerLibrary CoreLibrary) - - if (${OPENSSL_SOURCES_LENGTH} GREATER 0) - target_link_libraries(UnitTests OpenSSL) - endif() - + target_link_libraries(UnitTests OpenSSL) endif() endif() + ##################################################################### ## Create the standalone DLL containing the Orthanc Client API ##################################################################### @@ -332,20 +322,13 @@ message(FATAL_ERROR "Support your platform here") endif() - # Copy the header file of the client library to the build directory - add_custom_command( - TARGET OrthancClient PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E copy - ${ORTHANC_ROOT}/OrthancCppClient/Package/AUTOGENERATED/OrthancCppClient.h - ${CMAKE_BINARY_DIR}) - install( TARGETS OrthancClient LIBRARY DESTINATION lib ) install( - FILES ${CMAKE_BINARY_DIR}/OrthancCppClient.h + FILES ${ORTHANC_ROOT}/OrthancCppClient/Package/AUTOGENERATED/OrthancCppClient.h DESTINATION include/orthanc ) endif() diff -r fe796b053863 -r 0e510ea3de31 Core/Cache/CacheIndex.h --- a/Core/Cache/CacheIndex.h Fri Sep 13 11:10:58 2013 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,250 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2013 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 -#include - -#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 - class CacheIndex : public boost::noncopyable - { - private: - typedef std::list< std::pair > Queue; - typedef std::map 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 - void CacheIndex::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 - void CacheIndex::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 - void CacheIndex::TagAsMostRecent(T id) - { - if (!Contains(id)) - { - throw OrthancException(ErrorCode_InexistentItem); - } - - typename Index::iterator it = index_.find(id); - assert(it != index_.end()); - - std::pair item = *(it->second); - - queue_.erase(it->second); - queue_.push_front(item); - index_[id] = queue_.begin(); - - CheckInvariants(); - } - - - template - Payload CacheIndex::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 - T CacheIndex::RemoveOldest(Payload& payload) - { - if (IsEmpty()) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - - std::pair 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; - } -} diff -r fe796b053863 -r 0e510ea3de31 Core/Cache/LeastRecentlyUsedIndex.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/Cache/LeastRecentlyUsedIndex.h Fri Sep 13 11:25:08 2013 +0200 @@ -0,0 +1,346 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2013 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 +#include + +#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 + class LeastRecentlyUsedIndex : public boost::noncopyable + { + private: + typedef std::list< std::pair > Queue; + typedef std::map 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()); + + void AddOrMakeMostRecent(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 MakeMostRecent(T id); + + void MakeMostRecent(T id, Payload updatedPayload); + + /** + * 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(); + + /** + * 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(); + } + + const T& GetOldest() const; + + const Payload& GetOldestPayload() const; + }; + + + + + /****************************************************************** + ** Implementation of the template + ******************************************************************/ + + template + void LeastRecentlyUsedIndex::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 + void LeastRecentlyUsedIndex::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 + void LeastRecentlyUsedIndex::MakeMostRecent(T id) + { + if (!Contains(id)) + { + throw OrthancException(ErrorCode_InexistentItem); + } + + typename Index::iterator it = index_.find(id); + assert(it != index_.end()); + + std::pair item = *(it->second); + + queue_.erase(it->second); + queue_.push_front(item); + index_[id] = queue_.begin(); + + CheckInvariants(); + } + + + template + void LeastRecentlyUsedIndex::AddOrMakeMostRecent(T id, Payload payload) + { + typename Index::iterator it = index_.find(id); + + if (it != index_.end()) + { + // Already existing. Make it most recent. + std::pair item = *(it->second); + item.second = payload; + queue_.erase(it->second); + queue_.push_front(item); + } + else + { + // New item + queue_.push_front(std::make_pair(id, payload)); + } + + index_[id] = queue_.begin(); + + CheckInvariants(); + } + + + template + void LeastRecentlyUsedIndex::MakeMostRecent(T id, Payload updatedPayload) + { + if (!Contains(id)) + { + throw OrthancException(ErrorCode_InexistentItem); + } + + typename Index::iterator it = index_.find(id); + assert(it != index_.end()); + + std::pair item = *(it->second); + item.second = updatedPayload; + + queue_.erase(it->second); + queue_.push_front(item); + index_[id] = queue_.begin(); + + CheckInvariants(); + } + + + template + Payload LeastRecentlyUsedIndex::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 + T LeastRecentlyUsedIndex::RemoveOldest(Payload& payload) + { + if (IsEmpty()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + std::pair 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; + } + + + template + T LeastRecentlyUsedIndex::RemoveOldest() + { + if (IsEmpty()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + std::pair item = queue_.back(); + T oldest = item.first; + + queue_.pop_back(); + assert(index_.find(oldest) != index_.end()); + index_.erase(oldest); + + CheckInvariants(); + + return oldest; + } + + + template + const T& LeastRecentlyUsedIndex::GetOldest() const + { + if (IsEmpty()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + return queue_.back().first; + } + + + template + const Payload& LeastRecentlyUsedIndex::GetOldestPayload() const + { + if (IsEmpty()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + return queue_.back().second; + } +} diff -r fe796b053863 -r 0e510ea3de31 Core/Cache/MemoryCache.cpp --- a/Core/Cache/MemoryCache.cpp Fri Sep 13 11:10:58 2013 +0200 +++ b/Core/Cache/MemoryCache.cpp Fri Sep 13 11:25:08 2013 +0200 @@ -46,7 +46,7 @@ { VLOG(1) << "Reusing a cache page"; assert(p != NULL); - index_.TagAsMostRecent(id); + index_.MakeMostRecent(id); return *p; } diff -r fe796b053863 -r 0e510ea3de31 Core/Cache/MemoryCache.h --- a/Core/Cache/MemoryCache.h Fri Sep 13 11:10:58 2013 +0200 +++ b/Core/Cache/MemoryCache.h Fri Sep 13 11:25:08 2013 +0200 @@ -33,7 +33,7 @@ #pragma once #include -#include "CacheIndex.h" +#include "LeastRecentlyUsedIndex.h" #include "ICachePageProvider.h" namespace Orthanc @@ -52,7 +52,7 @@ ICachePageProvider& provider_; size_t cacheSize_; - CacheIndex index_; + LeastRecentlyUsedIndex index_; Page& Load(const std::string& id); diff -r fe796b053863 -r 0e510ea3de31 NEWS --- a/NEWS Fri Sep 13 11:10:58 2013 +0200 +++ b/NEWS Fri Sep 13 11:25:08 2013 +0200 @@ -1,6 +1,11 @@ Pending changes in the mainline =============================== +* Detection of stable patients/studies/series +* C-Find SCU at the instance level +* Link from modified to original resource in Orthanc Explorer +* Fix of issue #8 +* Anonymization of the medical alerts tag (0010,2000) Version 0.6.0 (2013/07/16) diff -r fe796b053863 -r 0e510ea3de31 OrthancExplorer/explorer.html --- a/OrthancExplorer/explorer.html Fri Sep 13 11:10:58 2013 +0200 +++ b/OrthancExplorer/explorer.html Fri Sep 13 11:25:08 2013 +0200 @@ -101,6 +101,9 @@
  • Before anonymization
  • +
  • + Before modification +
  • Download ZIP
  • @@ -144,6 +147,9 @@
  • Before anonymization
  • +
  • + Before modification +
  • Download ZIP
  • @@ -189,6 +195,9 @@
  • Before anonymization
  • +
  • + Before modification +
  • Preview this series
  • Download ZIP
  • @@ -231,6 +240,12 @@
    • Access
    • +
    • + Before anonymization +
    • +
    • + Before modification +
    • Download the DICOM file
    • Download the JSON file
    • Preview the instance
    • diff -r fe796b053863 -r 0e510ea3de31 OrthancExplorer/explorer.js --- a/OrthancExplorer/explorer.js Fri Sep 13 11:10:58 2013 +0200 +++ b/OrthancExplorer/explorer.js Fri Sep 13 11:25:08 2013 +0200 @@ -361,14 +361,13 @@ -function SetupAnonymizedFrom(buttonSelector, resource, resourceType) +function SetupAnonymizedOrModifiedFrom(buttonSelector, resource, resourceType, field) { - if ('AnonymizedFrom' in resource) + if (field in resource) { $(buttonSelector).closest('li').show(); $(buttonSelector).click(function(e) { - window.location.assign('explorer.html#' + resourceType + '?uuid=' + resource.AnonymizedFrom); - //window.location.reload(); + window.location.assign('explorer.html#' + resourceType + '?uuid=' + resource[field]); }); } else @@ -378,6 +377,7 @@ } + function RefreshPatient() { if ($.mobile.pageData) { @@ -404,7 +404,8 @@ target.append(FormatStudy(studies[i], '#study?uuid=' + studies[i].ID)); } - SetupAnonymizedFrom('#patient-anonymized-from', patient, 'patient'); + SetupAnonymizedOrModifiedFrom('#patient-anonymized-from', patient, 'patient', 'AnonymizedFrom'); + SetupAnonymizedOrModifiedFrom('#patient-modified-from', patient, 'patient', 'ModifiedFrom'); target.listview('refresh'); @@ -446,7 +447,8 @@ .append(FormatStudy(study)) .listview('refresh'); - SetupAnonymizedFrom('#study-anonymized-from', study, 'study'); + SetupAnonymizedOrModifiedFrom('#study-anonymized-from', study, 'study', 'AnonymizedFrom'); + SetupAnonymizedOrModifiedFrom('#study-modified-from', study, 'study', 'ModifiedFrom'); var target = $('#list-series'); $('li', target).remove(); @@ -491,7 +493,8 @@ .append(FormatSeries(series)) .listview('refresh'); - SetupAnonymizedFrom('#series-anonymized-from', series, 'series'); + SetupAnonymizedOrModifiedFrom('#series-anonymized-from', series, 'series', 'AnonymizedFrom'); + SetupAnonymizedOrModifiedFrom('#series-modified-from', series, 'series', 'ModifiedFrom'); var target = $('#list-instances'); $('li', target).remove(); @@ -595,6 +598,9 @@ } }); + SetupAnonymizedOrModifiedFrom('#instance-anonymized-from', instance, 'instance', 'AnonymizedFrom'); + SetupAnonymizedOrModifiedFrom('#instance-modified-from', instance, 'instance', 'ModifiedFrom'); + currentPage = 'instance'; currentUuid = $.mobile.pageData.uuid; }); diff -r fe796b053863 -r 0e510ea3de31 OrthancServer/DatabaseWrapper.cpp --- a/OrthancServer/DatabaseWrapper.cpp Fri Sep 13 11:10:58 2013 +0200 +++ b/OrthancServer/DatabaseWrapper.cpp Fri Sep 13 11:25:08 2013 +0200 @@ -949,4 +949,50 @@ { db_.Execute("DELETE FROM " + tableName); } + + + bool DatabaseWrapper::IsExistingResource(int64_t internalId) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT * FROM Resources WHERE internalId=?"); + s.BindInt(0, internalId); + return s.Step(); + } + + + void DatabaseWrapper::LookupTagValue(std::list& result, + DicomTag tag, + const std::string& value) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT id FROM MainDicomTags WHERE tagGroup=? AND tagElement=? and value=?"); + + s.BindInt(0, tag.GetGroup()); + s.BindInt(1, tag.GetElement()); + s.BindString(2, value); + + result.clear(); + + while (s.Step()) + { + result.push_back(s.ColumnInt64(0)); + } + } + + + void DatabaseWrapper::LookupTagValue(std::list& result, + const std::string& value) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT id FROM MainDicomTags WHERE value=?"); + + s.BindString(0, value); + + result.clear(); + + while (s.Step()) + { + result.push_back(s.ColumnInt64(0)); + } + } } diff -r fe796b053863 -r 0e510ea3de31 OrthancServer/DatabaseWrapper.h --- a/OrthancServer/DatabaseWrapper.h Fri Sep 13 11:10:58 2013 +0200 +++ b/OrthancServer/DatabaseWrapper.h Fri Sep 13 11:25:08 2013 +0200 @@ -223,5 +223,14 @@ uint64_t IncrementGlobalSequence(GlobalProperty property); void ClearTable(const std::string& tableName); + + bool IsExistingResource(int64_t internalId); + + void LookupTagValue(std::list& result, + DicomTag tag, + const std::string& value); + + void LookupTagValue(std::list& result, + const std::string& value); }; } diff -r fe796b053863 -r 0e510ea3de31 OrthancServer/DicomProtocol/DicomUserConnection.cpp --- a/OrthancServer/DicomProtocol/DicomUserConnection.cpp Fri Sep 13 11:10:58 2013 +0200 +++ b/OrthancServer/DicomProtocol/DicomUserConnection.cpp Fri Sep 13 11:25:08 2013 +0200 @@ -294,7 +294,20 @@ break; case FindRootModel_Instance: - DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "INSTANCE"); + if (manufacturer_ == ModalityManufacturer_ClearCanvas) + { + // This is a particular case for ClearCanvas, thanks to Peter Somlo . + // https://groups.google.com/d/msg/orthanc-users/j-6C3MAVwiw/iolB9hclom8J + // http://www.clearcanvas.ca/Home/Community/OldForums/tabid/526/aff/11/aft/14670/afv/topic/Default.aspx + printf("CLEAR CANVAS\n"); + DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "IMAGE"); + } + else + { + printf("GENERIC\n"); + DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "INSTANCE"); + } + sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; // Accession number @@ -453,6 +466,7 @@ distantAet_ = "ANY-SCP"; distantPort_ = 104; distantHost_ = "127.0.0.1"; + manufacturer_ = ModalityManufacturer_Generic; pimpl_->net_ = NULL; pimpl_->params_ = NULL; @@ -476,6 +490,12 @@ distantAet_ = aet; } + void DicomUserConnection::SetDistantManufacturer(ModalityManufacturer manufacturer) + { + Close(); + manufacturer_ = manufacturer; + } + void DicomUserConnection::SetDistantHost(const std::string& host) { diff -r fe796b053863 -r 0e510ea3de31 OrthancServer/DicomProtocol/DicomUserConnection.h --- a/OrthancServer/DicomProtocol/DicomUserConnection.h Fri Sep 13 11:10:58 2013 +0200 +++ b/OrthancServer/DicomProtocol/DicomUserConnection.h Fri Sep 13 11:25:08 2013 +0200 @@ -33,6 +33,7 @@ #pragma once #include "DicomFindAnswers.h" +#include "../ServerEnumerations.h" #include #include @@ -59,6 +60,7 @@ std::string distantAet_; std::string distantHost_; uint16_t distantPort_; + ModalityManufacturer manufacturer_; void CheckIsOpen() const; @@ -106,6 +108,13 @@ return distantPort_; } + void SetDistantManufacturer(ModalityManufacturer manufacturer); + + ModalityManufacturer GetDistantManufacturer() const + { + return manufacturer_; + } + void Open(); void Close(); diff -r fe796b053863 -r 0e510ea3de31 OrthancServer/Internals/CommandDispatcher.cpp --- a/OrthancServer/Internals/CommandDispatcher.cpp Fri Sep 13 11:10:58 2013 +0200 +++ b/OrthancServer/Internals/CommandDispatcher.cpp Fri Sep 13 11:25:08 2013 +0200 @@ -41,9 +41,149 @@ #include #include +#define ORTHANC_PROMISCUOUS 1 + static OFBool opt_rejectWithoutImplementationUID = OFFalse; + +#if ORTHANC_PROMISCUOUS == 1 +static +DUL_PRESENTATIONCONTEXT * +findPresentationContextID(LST_HEAD * head, + T_ASC_PresentationContextID presentationContextID) +{ + DUL_PRESENTATIONCONTEXT *pc; + LST_HEAD **l; + OFBool found = OFFalse; + + if (head == NULL) + return NULL; + + l = &head; + if (*l == NULL) + return NULL; + + pc = OFstatic_cast(DUL_PRESENTATIONCONTEXT *, LST_Head(l)); + (void)LST_Position(l, OFstatic_cast(LST_NODE *, pc)); + + while (pc && !found) { + if (pc->presentationContextID == presentationContextID) { + found = OFTrue; + } else { + pc = OFstatic_cast(DUL_PRESENTATIONCONTEXT *, LST_Next(l)); + } + } + return pc; +} + + +/** accept all presenstation contexts for unknown SOP classes, + * i.e. UIDs appearing in the list of abstract syntaxes + * where no corresponding name is defined in the UID dictionary. + * @param params pointer to association parameters structure + * @param transferSyntax transfer syntax to accept + * @param acceptedRole SCU/SCP role to accept + */ +static OFCondition acceptUnknownContextsWithTransferSyntax( + T_ASC_Parameters * params, + const char* transferSyntax, + T_ASC_SC_ROLE acceptedRole) +{ + OFCondition cond = EC_Normal; + int n, i, k; + DUL_PRESENTATIONCONTEXT *dpc; + T_ASC_PresentationContext pc; + OFBool accepted = OFFalse; + OFBool abstractOK = OFFalse; + + n = ASC_countPresentationContexts(params); + for (i = 0; i < n; i++) + { + cond = ASC_getPresentationContext(params, i, &pc); + if (cond.bad()) return cond; + abstractOK = OFFalse; + accepted = OFFalse; + + if (dcmFindNameOfUID(pc.abstractSyntax) == NULL) + { + abstractOK = OFTrue; + + /* check the transfer syntax */ + for (k = 0; (k < OFstatic_cast(int, pc.transferSyntaxCount)) && !accepted; k++) + { + if (strcmp(pc.proposedTransferSyntaxes[k], transferSyntax) == 0) + { + accepted = OFTrue; + } + } + } + + if (accepted) + { + cond = ASC_acceptPresentationContext( + params, pc.presentationContextID, + transferSyntax, acceptedRole); + if (cond.bad()) return cond; + } else { + T_ASC_P_ResultReason reason; + + /* do not refuse if already accepted */ + dpc = findPresentationContextID(params->DULparams.acceptedPresentationContext, + pc.presentationContextID); + if ((dpc == NULL) || ((dpc != NULL) && (dpc->result != ASC_P_ACCEPTANCE))) + { + + if (abstractOK) { + reason = ASC_P_TRANSFERSYNTAXESNOTSUPPORTED; + } else { + reason = ASC_P_ABSTRACTSYNTAXNOTSUPPORTED; + } + /* + * If previously this presentation context was refused + * because of bad transfer syntax let it stay that way. + */ + if ((dpc != NULL) && (dpc->result == ASC_P_TRANSFERSYNTAXESNOTSUPPORTED)) + reason = ASC_P_TRANSFERSYNTAXESNOTSUPPORTED; + + cond = ASC_refusePresentationContext(params, pc.presentationContextID, reason); + if (cond.bad()) return cond; + } + } + } + return EC_Normal; +} + + +/** accept all presenstation contexts for unknown SOP classes, + * i.e. UIDs appearing in the list of abstract syntaxes + * where no corresponding name is defined in the UID dictionary. + * This method is passed a list of "preferred" transfer syntaxes. + * @param params pointer to association parameters structure + * @param transferSyntax transfer syntax to accept + * @param acceptedRole SCU/SCP role to accept + */ +static OFCondition acceptUnknownContextsWithPreferredTransferSyntaxes( + T_ASC_Parameters * params, + const char* transferSyntaxes[], int transferSyntaxCount, + T_ASC_SC_ROLE acceptedRole = ASC_SC_ROLE_DEFAULT) +{ + OFCondition cond = EC_Normal; + /* + ** Accept in the order "least wanted" to "most wanted" transfer + ** syntax. Accepting a transfer syntax will override previously + ** accepted transfer syntaxes. + */ + for (int i = transferSyntaxCount - 1; i >= 0; i--) + { + cond = acceptUnknownContextsWithTransferSyntax(params, transferSyntaxes[i], acceptedRole); + if (cond.bad()) return cond; + } + return cond; +} +#endif + + namespace Orthanc { namespace Internals @@ -150,6 +290,18 @@ return NULL; } +#if ORTHANC_PROMISCUOUS == 1 + /* accept everything not known not to be a storage SOP class */ + cond = acceptUnknownContextsWithPreferredTransferSyntaxes( + assoc->params, transferSyntaxes, numTransferSyntaxes); + if (cond.bad()) + { + LOG(INFO) << cond.text(); + AssociationCleanup(assoc); + return NULL; + } +#endif + /* set our app title */ ASC_setAPTitles(assoc->params, NULL, NULL, server.GetApplicationEntityTitle().c_str()); diff -r fe796b053863 -r 0e510ea3de31 OrthancServer/Internals/StoreScp.cpp --- a/OrthancServer/Internals/StoreScp.cpp Fri Sep 13 11:10:58 2013 +0200 +++ b/OrthancServer/Internals/StoreScp.cpp Fri Sep 13 11:25:08 2013 +0200 @@ -117,7 +117,7 @@ FromDcmtkBridge::Convert(summary, **imageDataSet); FromDcmtkBridge::ToJson(dicomJson, **imageDataSet); - if (FromDcmtkBridge::SaveToMemoryBuffer(buffer, *imageDataSet) < 0) + if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer, *imageDataSet)) { LOG(ERROR) << "cannot write DICOM file to memory"; rsp->DimseStatus = STATUS_STORE_Refused_OutOfResources; diff -r fe796b053863 -r 0e510ea3de31 OrthancServer/OrthancInitialization.cpp --- a/OrthancServer/OrthancInitialization.cpp Fri Sep 13 11:10:58 2013 +0200 +++ b/OrthancServer/OrthancInitialization.cpp Fri Sep 13 11:25:08 2013 +0200 @@ -230,7 +230,8 @@ void GetDicomModality(const std::string& name, std::string& aet, std::string& address, - int& port) + int& port, + ModalityManufacturer& manufacturer) { boost::mutex::scoped_lock lock(globalMutex_); @@ -241,7 +242,8 @@ const Json::Value& modalities = (*configuration_) ["DicomModalities"]; if (modalities.type() != Json::objectValue || - !modalities.isMember(name)) + !modalities.isMember(name) || + (modalities[name].size() != 3 && modalities[name].size() != 4)) { throw OrthancException(""); } @@ -251,6 +253,15 @@ aet = modalities[name].get(0u, "").asString(); address = modalities[name].get(1u, "").asString(); port = modalities[name].get(2u, "").asInt(); + + if (modalities[name].size() == 4) + { + manufacturer = StringToModalityManufacturer(modalities[name].get(3u, "").asString()); + } + else + { + manufacturer = ModalityManufacturer_Generic; + } } catch (...) { diff -r fe796b053863 -r 0e510ea3de31 OrthancServer/OrthancInitialization.h --- a/OrthancServer/OrthancInitialization.h Fri Sep 13 11:10:58 2013 +0200 +++ b/OrthancServer/OrthancInitialization.h Fri Sep 13 11:25:08 2013 +0200 @@ -37,6 +37,7 @@ #include #include #include "../Core/HttpServer/MongooseServer.h" +#include "ServerEnumerations.h" namespace Orthanc { @@ -56,7 +57,8 @@ void GetDicomModality(const std::string& name, std::string& aet, std::string& address, - int& port); + int& port, + ModalityManufacturer& manufacturer); void GetOrthancPeer(const std::string& name, std::string& url, diff -r fe796b053863 -r 0e510ea3de31 OrthancServer/OrthancRestApi.cpp --- a/OrthancServer/OrthancRestApi.cpp Fri Sep 13 11:10:58 2013 +0200 +++ b/OrthancServer/OrthancRestApi.cpp Fri Sep 13 11:25:08 2013 +0200 @@ -76,11 +76,13 @@ { std::string aet, address; int port; - GetDicomModality(name, aet, address, port); + ModalityManufacturer manufacturer; + GetDicomModality(name, aet, address, port, manufacturer); connection.SetLocalApplicationEntityTitle(GetGlobalStringParameter("DicomAet", "ORTHANC")); connection.SetDistantApplicationEntityTitle(aet); connection.SetDistantHost(address); connection.SetDistantPort(port); + connection.SetDistantManufacturer(manufacturer); connection.Open(); } @@ -179,6 +181,34 @@ call.GetOutput().AnswerJson(result); } + static void DicomFindInstance(RestApi::PostCall& call) + { + DicomMap m; + DicomMap::SetupFindInstanceTemplate(m); + if (!MergeQueryAndTemplate(m, call.GetPostBody())) + { + return; + } + + if ((m.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 && + m.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2) || + m.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString().size() <= 2 || + m.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).AsString().size() <= 2) + { + return; + } + + DicomUserConnection connection; + ConnectToModality(connection, call.GetUriComponent("id", "")); + + DicomFindAnswers answers; + connection.FindInstance(answers, m); + + Json::Value result; + answers.ToJson(result); + call.GetOutput().AnswerJson(result); + } + static void DicomFind(RestApi::PostCall& call) { DicomMap m; @@ -934,6 +964,7 @@ result.append("find-patient"); result.append("find-study"); result.append("find-series"); + result.append("find-instance"); result.append("find"); result.append("store"); call.GetOutput().AnswerJson(result); @@ -1002,8 +1033,6 @@ throw OrthancException(ErrorCode_BadRequest); } - target.clear(); - for (Json::Value::ArrayIndex i = 0; i < removals.size(); i++) { std::string name = removals[i].asString(); @@ -1023,8 +1052,6 @@ throw OrthancException(ErrorCode_BadRequest); } - target.clear(); - Json::Value::Members members = replacements.getMemberNames(); for (size_t i = 0; i < members.size(); i++) { @@ -1070,7 +1097,7 @@ removals.insert(DicomTag(0x0008, 0x1155)); // Referenced SOP Instance UID removals.insert(DicomTag(0x0008, 0x2111)); // Derivation Description removals.insert(DicomTag(0x0010, 0x0010)); // Patient's Name - removals.insert(DicomTag(0x0010, 0x0020)); // Patient ID + //removals.insert(DicomTag(0x0010, 0x0020)); // Patient ID => cf. below (*) removals.insert(DicomTag(0x0010, 0x0030)); // Patient's Birth Date removals.insert(DicomTag(0x0010, 0x0032)); // Patient's Birth Time removals.insert(DicomTag(0x0010, 0x0040)); // Patient's Sex @@ -1086,8 +1113,8 @@ removals.insert(DicomTag(0x0010, 0x4000)); // Patient Comments removals.insert(DicomTag(0x0018, 0x1000)); // Device Serial Number removals.insert(DicomTag(0x0018, 0x1030)); // Protocol Name - //removals.insert(DicomTag(0x0020, 0x000d)); // Study Instance UID => generated below - //removals.insert(DicomTag(0x0020, 0x000e)); // Series Instance UID => generated below + //removals.insert(DicomTag(0x0020, 0x000d)); // Study Instance UID => cf. below (*) + //removals.insert(DicomTag(0x0020, 0x000e)); // Series Instance UID => cf. below (*) removals.insert(DicomTag(0x0020, 0x0010)); // Study ID removals.insert(DicomTag(0x0020, 0x0052)); // Frame of Reference UID removals.insert(DicomTag(0x0020, 0x0200)); // Synchronization Frame of Reference UID @@ -1099,30 +1126,25 @@ removals.insert(DicomTag(0x3006, 0x0024)); // Referenced Frame of Reference UID removals.insert(DicomTag(0x3006, 0x00c2)); // Related Frame of Reference UID + /** + * (*) Patient ID, Study Instance UID and Series Instance UID + * are modified by "AnonymizeInstance()" if anonymizing a single + * instance, or by "RetrieveMappedUid()" if anonymizing a + * patient/study/series. + **/ + + // Some more removals (from the experience of DICOM files at the CHU of Liege) removals.insert(DicomTag(0x0010, 0x1040)); // Patient's Address removals.insert(DicomTag(0x0032, 0x1032)); // Requesting Physician removals.insert(DicomTag(0x0010, 0x2154)); // PatientTelephoneNumbers + removals.insert(DicomTag(0x0010, 0x2000)); // Medical Alerts // Set the DeidentificationMethod tag replacements.insert(std::make_pair(DicomTag(0x0012, 0x0063), "Orthanc " ORTHANC_VERSION " - PS 3.15-2008 Table E.1-1")); - // Set the PatientIdentityRemoved + // Set the PatientIdentityRemoved tag replacements.insert(std::make_pair(DicomTag(0x0012, 0x0062), "YES")); - - // Generate random study UID if not specified - if (replacements.find(DICOM_TAG_STUDY_INSTANCE_UID) == replacements.end()) - { - replacements.insert(std::make_pair(DICOM_TAG_STUDY_INSTANCE_UID, - FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Study))); - } - - // Generate random series UID if not specified - if (replacements.find(DICOM_TAG_SERIES_INSTANCE_UID) == replacements.end()) - { - replacements.insert(std::make_pair(DICOM_TAG_SERIES_INSTANCE_UID, - FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Series))); - } } @@ -1225,13 +1247,6 @@ replacements.insert(std::make_pair(DicomTag(0x0010, 0x0010), GeneratePatientName(context))); } - // Generate random Patient's ID if none is specified - if (replacements.find(DICOM_TAG_PATIENT_ID) == replacements.end()) - { - replacements.insert(std::make_pair(DICOM_TAG_PATIENT_ID, - FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Patient))); - } - return true; } else @@ -1264,14 +1279,23 @@ UidMap& uidMap) { std::auto_ptr tag; - if (level == DicomRootLevel_Series) + + switch (level) { - tag.reset(new DicomTag(DICOM_TAG_SERIES_INSTANCE_UID)); - } - else - { - assert(level == DicomRootLevel_Study); - tag.reset(new DicomTag(DICOM_TAG_STUDY_INSTANCE_UID)); + case DicomRootLevel_Series: + tag.reset(new DicomTag(DICOM_TAG_SERIES_INSTANCE_UID)); + break; + + case DicomRootLevel_Study: + tag.reset(new DicomTag(DICOM_TAG_STUDY_INSTANCE_UID)); + break; + + case DicomRootLevel_Patient: + tag.reset(new DicomTag(DICOM_TAG_PATIENT_ID)); + break; + + default: + throw OrthancException(ErrorCode_InternalError); } std::string original; @@ -1301,7 +1325,8 @@ } - static void AnonymizeOrModifyResource(Removals& removals, + static void AnonymizeOrModifyResource(bool isAnonymization, + Removals& removals, Replacements& replacements, bool removePrivateTags, MetadataType metadataType, @@ -1338,15 +1363,20 @@ LOG(INFO) << "Modifying instance " << *it; ParsedDicomFile& original = context.GetDicomFile(*it); - bool isNewSeries = RetrieveMappedUid(original, DicomRootLevel_Series, replacements, uidMap); + DicomInstanceHasher originalHasher = original.GetHasher(); - bool isNewStudy = false; - if (resourceType == ResourceType_Study || - resourceType == ResourceType_Patient) + if (isFirst && !isAnonymization) { - isNewStudy = RetrieveMappedUid(original, DicomRootLevel_Study, replacements, uidMap); + // If modifying a study or a series, keep the original patient ID. + std::string patientId = originalHasher.GetPatientId(); + uidMap[std::make_pair(DicomRootLevel_Patient, patientId)] = patientId; } + bool isNewSeries = RetrieveMappedUid(original, DicomRootLevel_Series, replacements, uidMap); + bool isNewStudy = RetrieveMappedUid(original, DicomRootLevel_Study, replacements, uidMap); + bool isNewPatient = RetrieveMappedUid(original, DicomRootLevel_Patient, replacements, uidMap); + + /** * Compute the resulting DICOM instance and store it into the Orthanc store. **/ @@ -1367,7 +1397,6 @@ **/ DicomInstanceHasher modifiedHasher = modified->GetHasher(); - DicomInstanceHasher originalHasher = original.GetHasher(); if (isNewSeries) { @@ -1381,6 +1410,12 @@ metadataType, originalHasher.HashStudy()); } + if (isNewPatient) + { + context.GetIndex().SetMetadata(modifiedHasher.HashPatient(), + metadataType, originalHasher.HashPatient()); + } + assert(*it == originalHasher.HashInstance()); assert(modifiedInstance == modifiedHasher.HashInstance()); context.GetIndex().SetMetadata(modifiedInstance, metadataType, *it); @@ -1392,9 +1427,6 @@ if (isFirst) { - context.GetIndex().SetMetadata(modifiedHasher.HashPatient(), - metadataType, originalHasher.HashPatient()); - std::string newId; switch (resourceType) @@ -1449,6 +1481,27 @@ if (ParseAnonymizationRequest(removals, replacements, removePrivateTags, call)) { + // Generate random patient ID if not specified + if (replacements.find(DICOM_TAG_PATIENT_ID) == replacements.end()) + { + replacements.insert(std::make_pair(DICOM_TAG_PATIENT_ID, + FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Patient))); + } + + // Generate random study UID if not specified + if (replacements.find(DICOM_TAG_STUDY_INSTANCE_UID) == replacements.end()) + { + replacements.insert(std::make_pair(DICOM_TAG_STUDY_INSTANCE_UID, + FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Study))); + } + + // Generate random series UID if not specified + if (replacements.find(DICOM_TAG_SERIES_INSTANCE_UID) == replacements.end()) + { + replacements.insert(std::make_pair(DICOM_TAG_SERIES_INSTANCE_UID, + FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel_Series))); + } + AnonymizeOrModifyInstance(removals, replacements, removePrivateTags, call); } } @@ -1462,7 +1515,7 @@ if (ParseModifyRequest(removals, replacements, removePrivateTags, call)) { - AnonymizeOrModifyResource(removals, replacements, removePrivateTags, + AnonymizeOrModifyResource(false, removals, replacements, removePrivateTags, MetadataType_ModifiedFrom, ChangeType_ModifiedSeries, ResourceType_Series, call); } @@ -1477,7 +1530,7 @@ if (ParseAnonymizationRequest(removals, replacements, removePrivateTags, call)) { - AnonymizeOrModifyResource(removals, replacements, removePrivateTags, + AnonymizeOrModifyResource(true, removals, replacements, removePrivateTags, MetadataType_AnonymizedFrom, ChangeType_AnonymizedSeries, ResourceType_Series, call); } @@ -1492,7 +1545,7 @@ if (ParseModifyRequest(removals, replacements, removePrivateTags, call)) { - AnonymizeOrModifyResource(removals, replacements, removePrivateTags, + AnonymizeOrModifyResource(false, removals, replacements, removePrivateTags, MetadataType_ModifiedFrom, ChangeType_ModifiedStudy, ResourceType_Study, call); } @@ -1507,14 +1560,14 @@ if (ParseAnonymizationRequest(removals, replacements, removePrivateTags, call)) { - AnonymizeOrModifyResource(removals, replacements, removePrivateTags, + AnonymizeOrModifyResource(true, removals, replacements, removePrivateTags, MetadataType_AnonymizedFrom, ChangeType_AnonymizedStudy, ResourceType_Study, call); } } - static void ModifyPatientInplace(RestApi::PostCall& call) + /*static void ModifyPatientInplace(RestApi::PostCall& call) { Removals removals; Replacements replacements; @@ -1522,11 +1575,11 @@ if (ParseModifyRequest(removals, replacements, removePrivateTags, call)) { - AnonymizeOrModifyResource(removals, replacements, removePrivateTags, + AnonymizeOrModifyResource(false, removals, replacements, removePrivateTags, MetadataType_ModifiedFrom, ChangeType_ModifiedPatient, ResourceType_Patient, call); } - } + }*/ static void AnonymizePatientInplace(RestApi::PostCall& call) @@ -1537,7 +1590,7 @@ if (ParseAnonymizationRequest(removals, replacements, removePrivateTags, call)) { - AnonymizeOrModifyResource(removals, replacements, removePrivateTags, + AnonymizeOrModifyResource(true, removals, replacements, removePrivateTags, MetadataType_AnonymizedFrom, ChangeType_AnonymizedPatient, ResourceType_Patient, call); } @@ -1794,6 +1847,7 @@ Register("/modalities/{id}/find-patient", DicomFindPatient); Register("/modalities/{id}/find-study", DicomFindStudy); Register("/modalities/{id}/find-series", DicomFindSeries); + Register("/modalities/{id}/find-instance", DicomFindInstance); Register("/modalities/{id}/find", DicomFind); Register("/modalities/{id}/store", DicomStore); @@ -1804,7 +1858,7 @@ Register("/instances/{id}/modify", ModifyInstance); Register("/series/{id}/modify", ModifySeriesInplace); Register("/studies/{id}/modify", ModifyStudyInplace); - Register("/patients/{id}/modify", ModifyPatientInplace); + //Register("/patients/{id}/modify", ModifyPatientInplace); Register("/instances/{id}/anonymize", AnonymizeInstance); Register("/series/{id}/anonymize", AnonymizeSeriesInplace); diff -r fe796b053863 -r 0e510ea3de31 OrthancServer/ServerEnumerations.cpp --- a/OrthancServer/ServerEnumerations.cpp Fri Sep 13 11:10:58 2013 +0200 +++ b/OrthancServer/ServerEnumerations.cpp Fri Sep 13 11:25:08 2013 +0200 @@ -205,6 +205,15 @@ case ChangeType_ModifiedPatient: return "ModifiedPatient"; + case ChangeType_StablePatient: + return "StablePatient"; + + case ChangeType_StableStudy: + return "StableStudy"; + + case ChangeType_StableSeries: + return "StableSeries"; + default: throw OrthancException(ErrorCode_ParameterOutOfRange); } @@ -247,4 +256,39 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } } + + + const char* EnumerationToString(ModalityManufacturer manufacturer) + { + switch (manufacturer) + { + case ModalityManufacturer_Generic: + return "Generic"; + + case ModalityManufacturer_ClearCanvas: + return "ClearCanvas"; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + ModalityManufacturer StringToModalityManufacturer(const std::string& manufacturer) + { + if (manufacturer == "Generic") + { + return ModalityManufacturer_Generic; + } + else if (manufacturer == "ClearCanvas") + { + return ModalityManufacturer_ClearCanvas; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + } diff -r fe796b053863 -r 0e510ea3de31 OrthancServer/ServerEnumerations.h --- a/OrthancServer/ServerEnumerations.h Fri Sep 13 11:10:58 2013 +0200 +++ b/OrthancServer/ServerEnumerations.h Fri Sep 13 11:25:08 2013 +0200 @@ -51,6 +51,12 @@ StoreStatus_FilteredOut // Removed by NewInstanceFilter }; + enum ModalityManufacturer + { + ModalityManufacturer_Generic, + ModalityManufacturer_ClearCanvas + }; + /** * WARNING: Do not change the explicit values in the enumerations @@ -100,7 +106,10 @@ ChangeType_ModifiedStudy = 8, ChangeType_ModifiedSeries = 9, ChangeType_AnonymizedPatient = 10, - ChangeType_ModifiedPatient = 11 + ChangeType_ModifiedPatient = 11, + ChangeType_StablePatient = 12, + ChangeType_StableStudy = 13, + ChangeType_StableSeries = 14 }; void InitializeServerEnumerations(); @@ -123,6 +132,10 @@ const char* EnumerationToString(ChangeType type); + const char* EnumerationToString(ModalityManufacturer manufacturer); + + ModalityManufacturer StringToModalityManufacturer(const std::string& manufacturer); + ResourceType GetParentResourceType(ResourceType type); ResourceType GetChildResourceType(ResourceType type); diff -r fe796b053863 -r 0e510ea3de31 OrthancServer/ServerIndex.cpp --- a/OrthancServer/ServerIndex.cpp Fri Sep 13 11:10:58 2013 +0200 +++ b/OrthancServer/ServerIndex.cpp Fri Sep 13 11:25:08 2013 +0200 @@ -37,6 +37,7 @@ #endif #include "EmbeddedResources.h" +#include "OrthancInitialization.h" #include "../Core/Toolbox.h" #include "../Core/Uuid.h" #include "../Core/DicomFormat/DicomArray.h" @@ -187,6 +188,27 @@ }; + struct ServerIndex::UnstableResourcePayload + { + Orthanc::ResourceType type_; + boost::posix_time::ptime time_; + + UnstableResourcePayload() : type_(Orthanc::ResourceType_Instance) + { + } + + UnstableResourcePayload(Orthanc::ResourceType type) : type_(type) + { + time_ = boost::posix_time::second_clock::local_time(); + } + + unsigned int GetAge() const + { + return (boost::posix_time::second_clock::local_time() - time_).total_seconds(); + } + }; + + bool ServerIndex::DeleteResource(Json::Value& target, const std::string& uuid, ResourceType expectedType) @@ -227,18 +249,40 @@ } - static void FlushThread(DatabaseWrapper* db, - boost::mutex* mutex, - unsigned int sleep) + void ServerIndex::FlushThread(ServerIndex* that) { + unsigned int sleep; + + try + { + std::string sleepString = that->db_->GetGlobalProperty(GlobalProperty_FlushSleep); + sleep = boost::lexical_cast(sleepString); + } + catch (boost::bad_lexical_cast&) + { + // By default, wait for 10 seconds before flushing + sleep = 10; + } + LOG(INFO) << "Starting the database flushing thread (sleep = " << sleep << ")"; - while (1) + unsigned int count = 0; + + while (!that->done_) { - boost::this_thread::sleep(boost::posix_time::seconds(sleep)); - boost::mutex::scoped_lock lock(*mutex); - db->FlushToDisk(); + boost::this_thread::sleep(boost::posix_time::seconds(1)); + count++; + if (count < sleep) + { + continue; + } + + boost::mutex::scoped_lock lock(that->mutex_); + that->db_->FlushToDisk(); + count = 0; } + + LOG(INFO) << "Stopping the database flushing thread"; } @@ -284,6 +328,7 @@ ServerIndex::ServerIndex(ServerContext& context, const std::string& dbPath) : + done_(false), maximumStorageSize_(0), maximumPatients_(0) { @@ -314,25 +359,24 @@ // execution of Orthanc StandaloneRecycling(); - unsigned int sleep; - try - { - std::string sleepString = db_->GetGlobalProperty(GlobalProperty_FlushSleep); - sleep = boost::lexical_cast(sleepString); - } - catch (boost::bad_lexical_cast&) - { - // By default, wait for 10 seconds before flushing - sleep = 10; - } - - flushThread_ = boost::thread(FlushThread, db_.get(), &mutex_, sleep); + flushThread_ = boost::thread(FlushThread, this); + unstableResourcesMonitorThread_ = boost::thread(UnstableResourcesMonitorThread, this); } ServerIndex::~ServerIndex() { - LOG(INFO) << "Stopping the database flushing thread"; + done_ = true; + + if (flushThread_.joinable()) + { + flushThread_.join(); + } + + if (unstableResourcesMonitorThread_.joinable()) + { + unstableResourcesMonitorThread_.join(); + } } @@ -479,7 +523,6 @@ db_->SetMetadata(series, MetadataType_LastUpdate, now); db_->SetMetadata(study, MetadataType_LastUpdate, now); db_->SetMetadata(patient, MetadataType_LastUpdate, now); - db_->SetMetadata(instance, MetadataType_Instance_RemoteAet, remoteAet); const DicomValue* value; @@ -501,6 +544,11 @@ db_->LogChange(ChangeType_CompletedSeries, series, ResourceType_Series); } + // Mark the parent resources of this instance as unstable + MarkAsUnstable(patient, ResourceType_Patient); + MarkAsUnstable(study, ResourceType_Study); + MarkAsUnstable(series, ResourceType_Series); + t.Commit(instanceSize); return StoreStatus_Success; @@ -756,6 +804,13 @@ if (tmp.size() != 0) result["ModifiedFrom"] = tmp; + if (type == ResourceType_Patient || + type == ResourceType_Study || + type == ResourceType_Series) + { + result["IsStable"] = !unstableResources_.Contains(id); + } + return true; } @@ -1343,4 +1398,112 @@ break; } } + + + void ServerIndex::UnstableResourcesMonitorThread(ServerIndex* that) + { + int stableAge = GetGlobalIntegerParameter("StableAge", 60); + if (stableAge <= 0) + { + stableAge = 60; + } + + LOG(INFO) << "Starting the monitor for stable resources (stable age = " << stableAge << ")"; + + while (!that->done_) + { + // Check for stable resources each second + boost::this_thread::sleep(boost::posix_time::seconds(1)); + + boost::mutex::scoped_lock lock(that->mutex_); + + while (!that->unstableResources_.IsEmpty() && + that->unstableResources_.GetOldestPayload().GetAge() > static_cast(stableAge)) + { + // This DICOM resource has not received any new instance for + // some time. It can be considered as stable. + + UnstableResourcePayload payload; + int64_t id = that->unstableResources_.RemoveOldest(payload); + + // Ensure that the resource is still existing before logging the change + if (that->db_->IsExistingResource(id)) + { + switch (payload.type_) + { + case Orthanc::ResourceType_Patient: + that->db_->LogChange(ChangeType_StablePatient, id, ResourceType_Patient); + break; + + case Orthanc::ResourceType_Study: + that->db_->LogChange(ChangeType_StableStudy, id, ResourceType_Study); + break; + + case Orthanc::ResourceType_Series: + that->db_->LogChange(ChangeType_StableSeries, id, ResourceType_Series); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + //LOG(INFO) << "Stable resource: " << EnumerationToString(payload.type_) << " " << id; + } + } + } + + LOG(INFO) << "Closing the monitor thread for stable resources"; + } + + + void ServerIndex::MarkAsUnstable(int64_t id, + Orthanc::ResourceType type) + { + // WARNING: Before calling this method, "mutex_" must be locked. + + assert(type == Orthanc::ResourceType_Patient || + type == Orthanc::ResourceType_Study || + type == Orthanc::ResourceType_Series); + + unstableResources_.AddOrMakeMostRecent(id, type); + //LOG(INFO) << "Unstable resource: " << EnumerationToString(type) << " " << id; + } + + + + void ServerIndex::LookupTagValue(std::list& result, + DicomTag tag, + const std::string& value) + { + result.clear(); + + boost::mutex::scoped_lock lock(mutex_); + + std::list id; + db_->LookupTagValue(id, tag, value); + + for (std::list::const_iterator + it = id.begin(); it != id.end(); it++) + { + result.push_back(db_->GetPublicId(*it)); + } + } + + + void ServerIndex::LookupTagValue(std::list& result, + const std::string& value) + { + result.clear(); + + boost::mutex::scoped_lock lock(mutex_); + + std::list id; + db_->LookupTagValue(id, value); + + for (std::list::const_iterator + it = id.begin(); it != id.end(); it++) + { + result.push_back(db_->GetPublicId(*it)); + } + } } diff -r fe796b053863 -r 0e510ea3de31 OrthancServer/ServerIndex.h --- a/OrthancServer/ServerIndex.h Fri Sep 13 11:10:58 2013 +0200 +++ b/OrthancServer/ServerIndex.h Fri Sep 13 11:25:08 2013 +0200 @@ -34,6 +34,7 @@ #include #include +#include "../Core/Cache/LeastRecentlyUsedIndex.h" #include "../Core/SQLite/Connection.h" #include "../Core/DicomFormat/DicomMap.h" #include "../Core/DicomFormat/DicomInstanceHasher.h" @@ -55,17 +56,25 @@ { private: class Transaction; + struct UnstableResourcePayload; + bool done_; boost::mutex mutex_; boost::thread flushThread_; + boost::thread unstableResourcesMonitorThread_; std::auto_ptr listener_; std::auto_ptr db_; + LeastRecentlyUsedIndex unstableResources_; uint64_t currentStorageSize_; uint64_t maximumStorageSize_; unsigned int maximumPatients_; + static void FlushThread(ServerIndex* that); + + static void UnstableResourcesMonitorThread(ServerIndex* that); + void MainDicomTagsToJson(Json::Value& result, int64_t resourceId); @@ -78,6 +87,9 @@ void StandaloneRecycling(); + void MarkAsUnstable(int64_t id, + Orthanc::ResourceType type); + public: typedef std::list Attachments; @@ -174,5 +186,12 @@ void GetStatistics(Json::Value& target, const std::string& publicId); + + void LookupTagValue(std::list& result, + DicomTag tag, + const std::string& value); + + void LookupTagValue(std::list& result, + const std::string& value); }; } diff -r fe796b053863 -r 0e510ea3de31 Resources/CMake/Compiler.cmake --- a/Resources/CMake/Compiler.cmake Fri Sep 13 11:10:58 2013 +0200 +++ b/Resources/CMake/Compiler.cmake Fri Sep 13 11:25:08 2013 +0200 @@ -29,28 +29,6 @@ if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") - if (DEBIAN_FORCE_HARDENING) - execute_process( - COMMAND dpkg-buildflags --get CPPFLAGS - OUTPUT_VARIABLE DEBIAN_CPP_FLAGS - OUTPUT_STRIP_TRAILING_WHITESPACE) - execute_process( - COMMAND dpkg-buildflags --get CFLAGS - OUTPUT_VARIABLE DEBIAN_C_FLAGS - OUTPUT_STRIP_TRAILING_WHITESPACE) - execute_process( - COMMAND dpkg-buildflags --get CXXFLAGS - OUTPUT_VARIABLE DEBIAN_CXX_FLAGS - OUTPUT_STRIP_TRAILING_WHITESPACE) - execute_process( - COMMAND dpkg-buildflags --get LDFLAGS - OUTPUT_VARIABLE DEBIAN_LD_FLAGS - OUTPUT_STRIP_TRAILING_WHITESPACE) - - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${DEBIAN_C_FLAGS} ${DEBIAN_CPP_FLAGS}") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${DEBIAN_CXX_FLAGS} ${DEBIAN_CPP_FLAGS}") - endif() - if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -nostdinc++ -I${LSB_PATH}/include -I${LSB_PATH}/include/c++ -I${LSB_PATH}/include/c++/backward -fpermissive") endif() @@ -59,9 +37,9 @@ -D_LARGEFILE64_SOURCE=1 -D_FILE_OFFSET_BITS=64 ) - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--as-needed ${DEBIAN_LD_FLAGS}") - set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--no-undefined ${DEBIAN_LD_FLAGS}") - set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined ${DEBIAN_LD_FLAGS}") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--as-needed") + set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--no-undefined") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined") # Remove the "-rdynamic" option # http://www.mail-archive.com/cmake@cmake.org/msg08837.html diff -r fe796b053863 -r 0e510ea3de31 Resources/CMake/MongooseConfiguration.cmake --- a/Resources/CMake/MongooseConfiguration.cmake Fri Sep 13 11:10:58 2013 +0200 +++ b/Resources/CMake/MongooseConfiguration.cmake Fri Sep 13 11:25:08 2013 +0200 @@ -35,9 +35,11 @@ endif() - if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows" AND ${CMAKE_COMPILER_IS_GNUCXX}) - # This is a patch for MinGW64 - add_definitions(-D_TIMESPEC_DEFINED=1) + if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + if (${CMAKE_COMPILER_IS_GNUCXX}) + # This is a patch for MinGW64 + add_definitions(-D_TIMESPEC_DEFINED=1) + endif() endif() source_group(ThirdParty\\Mongoose REGULAR_EXPRESSION ${MONGOOSE_SOURCES_DIR}/.*) diff -r fe796b053863 -r 0e510ea3de31 Resources/Configuration.json --- a/Resources/Configuration.json Fri Sep 13 11:10:58 2013 +0200 +++ b/Resources/Configuration.json Fri Sep 13 11:25:08 2013 +0200 @@ -33,13 +33,6 @@ "LuaScripts" : [ ], - // Dictionary of symbolic names for the user-defined metadata. Each - // entry must map a number between 1024 and 65535 to an unique - // string. - "UserMetadata" : { - // "Sample" : 1024 - }, - /** @@ -103,6 +96,14 @@ * command line "storescp 2000". **/ // "sample" : [ "STORESCP", "localhost", 2000 ] + + /** + * A fourth parameter is available to enable patches for a + * specific PACS manufacturer. The allowed values are currently + * "Generic" (default value) and "ClearCanvas". This parameter is + * case-sensitive. + **/ + // "clearcanvas" : [ "CLEARCANVAS", "192.168.1.1", 104, "ClearCanvas" ] }, // The list of the known Orthanc peers @@ -114,5 +115,22 @@ **/ // "peer" : [ "http://localhost:8043/", "alice", "alicePassword" ] // "peer2" : [ "http://localhost:8044/" ] - } + }, + + + + /** + * Advanced options + **/ + + // Dictionary of symbolic names for the user-defined metadata. Each + // entry must map a number between 1024 and 65535 to an unique + // string. + "UserMetadata" : { + // "Sample" : 1024 + }, + + // Number of seconds without receiving any instance before a + // patient, a study or a series is considered as stable. + "StableAge" : 60 } diff -r fe796b053863 -r 0e510ea3de31 Resources/EclipseCodingStyle.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/EclipseCodingStyle.xml Fri Sep 13 11:25:08 2013 +0200 @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r fe796b053863 -r 0e510ea3de31 Resources/Samples/RestApi/CMakeLists.txt --- a/Resources/Samples/RestApi/CMakeLists.txt Fri Sep 13 11:10:58 2013 +0200 +++ b/Resources/Samples/RestApi/CMakeLists.txt Fri Sep 13 11:25:08 2013 +0200 @@ -22,8 +22,8 @@ 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) + CMAKE_ARGS -DSTATIC_BUILD=ON -DSTANDALONE_BUILD=ON -DUSE_DYNAMIC_GOOGLE_LOG=OFF -DUSE_DYNAMIC_SQLITE=OFF -DENABLE_SSL=OFF ${TOOLCHAIN} + BUILD_COMMAND $(MAKE) CoreLibrary INSTALL_COMMAND "" BUILD_IN_SOURCE 0 ) diff -r fe796b053863 -r 0e510ea3de31 THANKS --- a/THANKS Fri Sep 13 11:10:58 2013 +0200 +++ b/THANKS Fri Sep 13 11:25:08 2013 +0200 @@ -16,6 +16,7 @@ * Will Ryder (will.ryder@sydney.edu.au), for improvements with the handling of series with temporal positions (fMRI and dynamic PET). * Ryan Walklin (ryanwalklin@gmail.com), for Mac OS X build. +* Peter Somlo (peter.somlo@gmail.com), for ClearCanvas support. Artwork diff -r fe796b053863 -r 0e510ea3de31 UnitTests/MemoryCache.cpp --- a/UnitTests/MemoryCache.cpp Fri Sep 13 11:10:58 2013 +0200 +++ b/UnitTests/MemoryCache.cpp Fri Sep 13 11:25:08 2013 +0200 @@ -8,46 +8,53 @@ #include "../Core/Cache/MemoryCache.h" -TEST(CacheIndex, Basic) +TEST(LRU, Basic) { - Orthanc::CacheIndex r; + Orthanc::LeastRecentlyUsedIndex 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"); + r.MakeMostRecent("a"); + r.MakeMostRecent("d"); + r.MakeMostRecent("b"); + r.MakeMostRecent("c"); + r.MakeMostRecent("d"); + r.MakeMostRecent("c"); + ASSERT_EQ("a", r.GetOldest()); ASSERT_EQ("a", r.RemoveOldest()); + ASSERT_EQ("b", r.GetOldest()); ASSERT_EQ("b", r.RemoveOldest()); + ASSERT_EQ("d", r.GetOldest()); ASSERT_EQ("d", r.RemoveOldest()); + ASSERT_EQ("c", r.GetOldest()); ASSERT_EQ("c", r.RemoveOldest()); ASSERT_TRUE(r.IsEmpty()); + + ASSERT_THROW(r.GetOldest(), Orthanc::OrthancException); + ASSERT_THROW(r.RemoveOldest(), Orthanc::OrthancException); } -TEST(CacheIndex, Payload) +TEST(LRU, Payload) { - Orthanc::CacheIndex r; + Orthanc::LeastRecentlyUsedIndex 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"); + r.MakeMostRecent("a"); + r.MakeMostRecent("d"); + r.MakeMostRecent("b"); + r.MakeMostRecent("c"); + r.MakeMostRecent("d"); + r.MakeMostRecent("c"); ASSERT_TRUE(r.Contains("b")); ASSERT_EQ(421, r.Invalidate("b")); @@ -58,14 +65,76 @@ ASSERT_TRUE(r.Contains("c", p)); ASSERT_EQ(422, p); ASSERT_TRUE(r.Contains("d", p)); ASSERT_EQ(423, p); + ASSERT_EQ("a", r.GetOldest()); + ASSERT_EQ(420, r.GetOldestPayload()); ASSERT_EQ("a", r.RemoveOldest(p)); ASSERT_EQ(420, p); + + ASSERT_EQ("d", r.GetOldest()); + ASSERT_EQ(423, r.GetOldestPayload()); ASSERT_EQ("d", r.RemoveOldest(p)); ASSERT_EQ(423, p); + + ASSERT_EQ("c", r.GetOldest()); + ASSERT_EQ(422, r.GetOldestPayload()); ASSERT_EQ("c", r.RemoveOldest(p)); ASSERT_EQ(422, p); ASSERT_TRUE(r.IsEmpty()); } +TEST(LRU, PayloadUpdate) +{ + Orthanc::LeastRecentlyUsedIndex r; + + r.Add("a", 420); + r.Add("b", 421); + r.Add("d", 423); + + r.MakeMostRecent("a", 424); + r.MakeMostRecent("d", 421); + + ASSERT_EQ("b", r.GetOldest()); + ASSERT_EQ(421, r.GetOldestPayload()); + r.RemoveOldest(); + + ASSERT_EQ("a", r.GetOldest()); + ASSERT_EQ(424, r.GetOldestPayload()); + r.RemoveOldest(); + + ASSERT_EQ("d", r.GetOldest()); + ASSERT_EQ(421, r.GetOldestPayload()); + r.RemoveOldest(); + + ASSERT_TRUE(r.IsEmpty()); +} + + + +TEST(LRU, PayloadUpdateBis) +{ + Orthanc::LeastRecentlyUsedIndex r; + + r.AddOrMakeMostRecent("a", 420); + r.AddOrMakeMostRecent("b", 421); + r.AddOrMakeMostRecent("d", 423); + r.AddOrMakeMostRecent("a", 424); + r.AddOrMakeMostRecent("d", 421); + + ASSERT_EQ("b", r.GetOldest()); + ASSERT_EQ(421, r.GetOldestPayload()); + r.RemoveOldest(); + + ASSERT_EQ("a", r.GetOldest()); + ASSERT_EQ(424, r.GetOldestPayload()); + r.RemoveOldest(); + + ASSERT_EQ("d", r.GetOldest()); + ASSERT_EQ(421, r.GetOldestPayload()); + r.RemoveOldest(); + + ASSERT_TRUE(r.IsEmpty()); +} + + namespace diff -r fe796b053863 -r 0e510ea3de31 UnitTests/ServerIndex.cpp --- a/UnitTests/ServerIndex.cpp Fri Sep 13 11:10:58 2013 +0200 +++ b/UnitTests/ServerIndex.cpp Fri Sep 13 11:25:08 2013 +0200 @@ -5,6 +5,7 @@ #include #include +#include using namespace Orthanc; @@ -432,3 +433,57 @@ ASSERT_EQ(3u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence)); ASSERT_EQ(4u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence)); } + + + +TEST(DatabaseWrapper, LookupTagValue) +{ + ServerIndexListener listener; + DatabaseWrapper index(listener); + + int64_t a[] = { + index.CreateResource("a", ResourceType_Study), // 0 + index.CreateResource("b", ResourceType_Study), // 1 + index.CreateResource("c", ResourceType_Study), // 2 + index.CreateResource("d", ResourceType_Series) // 3 + }; + + DicomMap m; + m.Clear(); m.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "0"); index.SetMainDicomTags(a[0], m); + m.Clear(); m.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "1"); index.SetMainDicomTags(a[1], m); + m.Clear(); m.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "0"); index.SetMainDicomTags(a[2], m); + m.Clear(); m.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, "0"); index.SetMainDicomTags(a[3], m); + + std::list s; + + index.LookupTagValue(s, DICOM_TAG_STUDY_INSTANCE_UID, "0"); + ASSERT_EQ(2u, s.size()); + ASSERT_TRUE(std::find(s.begin(), s.end(), a[0]) != s.end()); + ASSERT_TRUE(std::find(s.begin(), s.end(), a[2]) != s.end()); + + index.LookupTagValue(s, "0"); + ASSERT_EQ(3u, s.size()); + ASSERT_TRUE(std::find(s.begin(), s.end(), a[0]) != s.end()); + ASSERT_TRUE(std::find(s.begin(), s.end(), a[2]) != s.end()); + ASSERT_TRUE(std::find(s.begin(), s.end(), a[3]) != s.end()); + + index.LookupTagValue(s, DICOM_TAG_STUDY_INSTANCE_UID, "1"); + ASSERT_EQ(1u, s.size()); + ASSERT_TRUE(std::find(s.begin(), s.end(), a[1]) != s.end()); + + index.LookupTagValue(s, "1"); + ASSERT_EQ(1u, s.size()); + ASSERT_TRUE(std::find(s.begin(), s.end(), a[1]) != s.end()); + + + /*{ + std::list s; + context.GetIndex().LookupTagValue(s, DICOM_TAG_STUDY_INSTANCE_UID, "1.2.250.1.74.20130819132500.29000036381059"); + for (std::list::iterator i = s.begin(); i != s.end(); i++) + { + std::cout << "*** " << *i << std::endl;; + } + }*/ + + +} diff -r fe796b053863 -r 0e510ea3de31 UnitTests/main.cpp --- a/UnitTests/main.cpp Fri Sep 13 11:10:58 2013 +0200 +++ b/UnitTests/main.cpp Fri Sep 13 11:25:08 2013 +0200 @@ -445,7 +445,7 @@ q.Enqueue(new DynamicInteger(20)); throw OrthancException("Nope"); } - catch (OrthancException) + catch (OrthancException&) { } }