Mercurial > hg > orthanc
changeset 290:b3322636b06d
merge
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Fri, 14 Dec 2012 11:24:24 +0100 |
parents | ffd98d2f0b91 (diff) b0734fbcdd97 (current diff) |
children | 4d7469f72a0b |
files | |
diffstat | 168 files changed, 12143 insertions(+), 3031 deletions(-) [+] |
line wrap: on
line diff
--- a/CMakeLists.txt Thu Oct 04 15:09:56 2012 +0200 +++ b/CMakeLists.txt Fri Dec 14 11:24:24 2012 +0100 @@ -1,34 +1,73 @@ cmake_minimum_required(VERSION 2.8) project(Orthanc) -include(${CMAKE_SOURCE_DIR}/Resources/CMake/AutoGeneratedCode.cmake) -include(${CMAKE_SOURCE_DIR}/Resources/CMake/DownloadPackage.cmake) + +# Version of the build, should always be "mainline" except in release branches +add_definitions( + -DORTHANC_VERSION="mainline" + ) + +# Parameters of the build +SET(STATIC_BUILD ON CACHE BOOL "Static build of the third-party libraries (necessary for Windows)") +SET(STANDALONE_BUILD OFF CACHE BOOL "Standalone build (all the resources are embedded, necessary for releases)") +SET(ENABLE_SSL ON CACHE BOOL "Include support for SSL") +SET(BUILD_UNIT_TESTS ON CACHE BOOL "Build the unit tests") + +# Advanced parameters (for Debian packaging) +SET(USE_DYNAMIC_JSONCPP OFF CACHE BOOL "Use the dynamic version of JsonCpp (only for Debian sid)") +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_SQLITE ON CACHE BOOL "Use the dynamic version of SQLite") +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") + +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) + +# Some basic inclusions include(CheckIncludeFiles) include(CheckIncludeFileCXX) +include(${CMAKE_SOURCE_DIR}/Resources/CMake/AutoGeneratedCode.cmake) +include(${CMAKE_SOURCE_DIR}/Resources/CMake/DownloadPackage.cmake) +include(${CMAKE_SOURCE_DIR}/Resources/CMake/Compiler.cmake) -SET(STATIC_BUILD ON CACHE BOOL "Static build of the third-party libraries (necessary for Windows)") -SET(STANDALONE_BUILD OFF CACHE BOOL "Standalone build (necessary for cross-compilation or binary releases)") -SET(ENABLE_SSL ON CACHE BOOL "Include support for SSL") -SET(DEBIAN_HARDENING OFF CACHE BOOL "Use Debian hardening flags") - +# Configuration of the standalone builds if (${CMAKE_CROSSCOMPILING}) + # Cross-compilation implies the standalone build SET(STANDALONE_BUILD ON) endif() -if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") - CHECK_INCLUDE_FILES(rpc.h HAVE_UUID_H) +if (${STANDALONE_BUILD}) + # We embed all the resources in the binaries for standalone builds + add_definitions(-DORTHANC_STANDALONE=1) + EmbedResources( + PREPARE_DATABASE OrthancServer/PrepareDatabase.sql + ORTHANC_EXPLORER OrthancExplorer + CONFIGURATION_SAMPLE Resources/Configuration.json + ) else() - CHECK_INCLUDE_FILES(uuid/uuid.h HAVE_UUID_H) -endif() - -if (NOT HAVE_UUID_H) - message(FATAL_ERROR "Please install the uuid-dev package") + add_definitions( + -DORTHANC_STANDALONE=0 + -DORTHANC_PATH=\"${CMAKE_SOURCE_DIR}\" + ) + EmbedResources( + PREPARE_DATABASE OrthancServer/PrepareDatabase.sql + CONFIGURATION_SAMPLE Resources/Configuration.json + ) endif() +# Prepare the third-party dependencies SET(THIRD_PARTY_SOURCES ${CMAKE_SOURCE_DIR}/Resources/md5/md5.c ${CMAKE_SOURCE_DIR}/Resources/base64/base64.cpp + ${CMAKE_SOURCE_DIR}/Resources/sha1/sha1.cpp ) include(${CMAKE_SOURCE_DIR}/Resources/CMake/GoogleLogConfiguration.cmake) @@ -41,8 +80,11 @@ endif() include(${CMAKE_SOURCE_DIR}/Resources/CMake/BoostConfiguration.cmake) -include(${CMAKE_SOURCE_DIR}/Resources/CMake/DcmtkConfiguration.cmake) -include(${CMAKE_SOURCE_DIR}/Resources/CMake/GoogleTestConfiguration.cmake) + +if(NOT ONLY_CORE_LIBRARY) + include(${CMAKE_SOURCE_DIR}/Resources/CMake/DcmtkConfiguration.cmake) +endif() + include(${CMAKE_SOURCE_DIR}/Resources/CMake/MongooseConfiguration.cmake) include(${CMAKE_SOURCE_DIR}/Resources/CMake/ZlibConfiguration.cmake) include(${CMAKE_SOURCE_DIR}/Resources/CMake/SQLiteConfiguration.cmake) @@ -51,126 +93,38 @@ include(${CMAKE_SOURCE_DIR}/Resources/CMake/LibPngConfiguration.cmake) -if (${CMAKE_COMPILER_IS_GNUCXX}) - set(CMAKE_C_FLAGS "-Wall -Wno-long-long -Wno-implicit-function-declaration") - # --std=c99 makes libcurl not to compile - # -pedantic gives a lot of warnings on OpenSSL - set(CMAKE_CXX_FLAGS "-Wall -pedantic -Wno-long-long -Wno-variadic-macros") -elseif (${MSVC}) - # http://stackoverflow.com/a/6510446 - foreach(flag_var - CMAKE_C_FLAGS_DEBUG - CMAKE_CXX_FLAGS_DEBUG - CMAKE_C_FLAGS_RELEASE - CMAKE_CXX_FLAGS_RELEASE - CMAKE_C_FLAGS_MINSIZEREL - CMAKE_CXX_FLAGS_MINSIZEREL - CMAKE_C_FLAGS_RELWITHDEBINFO - CMAKE_CXX_FLAGS_RELWITHDEBINFO) - string(REGEX REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}") - string(REGEX REPLACE "/MDd" "/MTd" ${flag_var} "${${flag_var}}") - endforeach(flag_var) - add_definitions( - -D_CRT_SECURE_NO_WARNINGS=1 - -D_CRT_SECURE_NO_DEPRECATE=1 - ) - include_directories(${CMAKE_SOURCE_DIR}/Resources/VisualStudio) - link_libraries(netapi32) -endif() - - -if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") - if (DEBIAN_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() - - add_definitions( - -D_LARGEFILE64_SOURCE=1 - -D_FILE_OFFSET_BITS=64 - ) - set(CMAKE_EXE_LINKER_FLAGS "-Wl,--as-needed ${DEBIAN_LD_FLAGS}") - set(CMAKE_MODULE_LINKER_FLAGS "-Wl,--no-undefined ${DEBIAN_LD_FLAGS}") - set(CMAKE_SHARED_LINKER_FLAGS "-Wl,--no-undefined ${DEBIAN_LD_FLAGS}") - - # http://www.mail-archive.com/cmake@cmake.org/msg08837.html - set(CMAKE_SHARED_LIBRARY_LINK_CXX_FLAGS "") - link_libraries(uuid pthread rt) - -elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Windows") - add_definitions( - -DWINVER=0x0501 - -D_CRT_SECURE_NO_WARNINGS=1 - ) - link_libraries(rpcrt4 ws2_32) -endif() - - -if (${STATIC_BUILD}) - add_definitions(-DORTHANC_STATIC=1) -else() - add_definitions(-DORTHANC_STATIC=0) -endif() - -if (${STANDALONE_BUILD}) - add_definitions( - -DORTHANC_STANDALONE=1 - ) - - EmbedResources( - PREPARE_DATABASE OrthancServer/PrepareDatabase.sql - ORTHANC_EXPLORER OrthancExplorer - ) - -else() - add_definitions( - -DORTHANC_STANDALONE=0 - -DORTHANC_PATH=\"${CMAKE_SOURCE_DIR}\" - ) - - EmbedResources( - PREPARE_DATABASE OrthancServer/PrepareDatabase.sql - ) -endif() - - +# The main instructions to build the Orthanc binaries add_library(CoreLibrary STATIC ${AUTOGENERATED_SOURCES} ${THIRD_PARTY_SOURCES} + Core/Cache/MemoryCache.cpp Core/ChunkedBuffer.cpp Core/Compression/BufferCompressor.cpp Core/Compression/ZlibCompressor.cpp Core/Compression/ZipWriter.cpp + Core/Compression/HierarchicalZipWriter.cpp Core/OrthancException.cpp Core/DicomFormat/DicomArray.cpp Core/DicomFormat/DicomMap.cpp Core/DicomFormat/DicomTag.cpp Core/DicomFormat/DicomIntegerPixelAccessor.cpp - Core/FileStorage.cpp + Core/DicomFormat/DicomInstanceHasher.cpp + Core/FileStorage/FileStorage.cpp + Core/FileStorage/StorageAccessor.cpp + Core/FileStorage/CompressedFileStorageAccessor.cpp + Core/FileStorage/FileStorageAccessor.cpp Core/HttpServer/EmbeddedResourceHttpHandler.cpp Core/HttpServer/FilesystemHttpHandler.cpp Core/HttpServer/HttpHandler.cpp Core/HttpServer/HttpOutput.cpp Core/HttpServer/MongooseServer.cpp + Core/HttpServer/HttpFileSender.cpp + Core/HttpServer/FilesystemHttpSender.cpp + Core/RestApi/RestApiPath.cpp + Core/RestApi/RestApiOutput.cpp + Core/RestApi/RestApi.cpp Core/MultiThreading/BagOfRunnablesBySteps.cpp Core/PngWriter.cpp Core/SQLite/Connection.cpp @@ -186,40 +140,65 @@ OrthancCppClient/HttpException.cpp ) -add_library(ServerLibrary - 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 - ) + + +if(NOT ONLY_CORE_LIBRARY) + add_library(ServerLibrary + ${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 + ) -# Ensure autogenerated code is built before building ServerLibrary -add_dependencies(ServerLibrary CoreLibrary) + target_link_libraries(Orthanc ServerLibrary CoreLibrary) -add_executable(Orthanc - OrthancServer/main.cpp - ) + install( + TARGETS Orthanc + RUNTIME DESTINATION bin + ) -add_executable(UnitTests - ${GTEST_SOURCES} - UnitTests/main.cpp - UnitTests/SQLite.cpp - UnitTests/SQLiteChromium.cpp - UnitTests/Versions.cpp - UnitTests/Zip.cpp - ) + # 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/RestApi.cpp + UnitTests/SQLite.cpp + UnitTests/SQLiteChromium.cpp + UnitTests/ServerIndex.cpp + UnitTests/Versions.cpp + UnitTests/Zip.cpp + UnitTests/FileStorage.cpp + UnitTests/MemoryCache.cpp + UnitTests/main.cpp + ) + target_link_libraries(UnitTests ServerLibrary CoreLibrary) + endif() +endif() -TARGET_LINK_LIBRARIES(Orthanc ServerLibrary CoreLibrary) -TARGET_LINK_LIBRARIES(UnitTests ServerLibrary CoreLibrary) +# Generate the Doxygen documentation if Doxygen is present find_package(Doxygen) if (DOXYGEN_FOUND) configure_file( @@ -231,4 +210,7 @@ WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} COMMENT "Generating API documentation with Doxygen" VERBATIM ) -endif(DOXYGEN_FOUND) +else() + message("Doxygen not found. The documentation will not be built.") +endif() +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/Cache/CacheIndex.h Fri Dec 14 11:24:24 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:24:24 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:24:24 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:24:24 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/ChunkedBuffer.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/ChunkedBuffer.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/Core/ChunkedBuffer.h Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/ChunkedBuffer.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/Core/Compression/BufferCompressor.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/Compression/BufferCompressor.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/Core/Compression/BufferCompressor.h Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/Compression/BufferCompressor.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/Compression/HierarchicalZipWriter.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,180 @@ +/** + * 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 "HierarchicalZipWriter.h" + +#include "../Toolbox.h" +#include "../OrthancException.h" + +#include <boost/lexical_cast.hpp> + +namespace Orthanc +{ + std::string HierarchicalZipWriter::Index::KeepAlphanumeric(const std::string& source) + { + std::string result; + + bool lastSpace = false; + + result.reserve(source.size()); + for (size_t i = 0; i < source.size(); i++) + { + char c = source[i]; + if (c == '^') + c = ' '; + + if (c < 128 && + c >= 0) + { + if (isspace(c)) + { + if (!lastSpace) + { + lastSpace = true; + result.push_back(' '); + } + } + else if (isalnum(c) || + c == '.' || + c == '_') + { + result.push_back(c); + lastSpace = false; + } + } + } + + return Toolbox::StripSpaces(result); + } + + std::string HierarchicalZipWriter::Index::GetCurrentDirectoryPath() const + { + std::string result; + + Stack::const_iterator it = stack_.begin(); + it++; // Skip the root node (to avoid absolute paths) + + while (it != stack_.end()) + { + result += (*it)->name_ + "/"; + it++; + } + + return result; + } + + std::string HierarchicalZipWriter::Index::EnsureUniqueFilename(const char* filename) + { + std::string standardized = KeepAlphanumeric(filename); + + Directory& d = *stack_.back(); + Directory::Content::iterator it = d.content_.find(standardized); + + if (it == d.content_.end()) + { + d.content_[standardized] = 1; + return standardized; + } + else + { + it->second++; + return standardized + "-" + boost::lexical_cast<std::string>(it->second); + } + } + + HierarchicalZipWriter::Index::Index() + { + stack_.push_back(new Directory); + } + + HierarchicalZipWriter::Index::~Index() + { + for (Stack::iterator it = stack_.begin(); it != stack_.end(); it++) + { + delete *it; + } + } + + std::string HierarchicalZipWriter::Index::OpenFile(const char* name) + { + return GetCurrentDirectoryPath() + EnsureUniqueFilename(name); + } + + void HierarchicalZipWriter::Index::OpenDirectory(const char* name) + { + std::string d = EnsureUniqueFilename(name); + + // Push the new directory onto the stack + stack_.push_back(new Directory); + stack_.back()->name_ = d; + } + + void HierarchicalZipWriter::Index::CloseDirectory() + { + if (IsRoot()) + { + // Cannot close the root node + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + delete stack_.back(); + stack_.pop_back(); + } + + + HierarchicalZipWriter::HierarchicalZipWriter(const char* path) + { + writer_.SetOutputPath(path); + writer_.Open(); + } + + HierarchicalZipWriter::~HierarchicalZipWriter() + { + writer_.Close(); + } + + void HierarchicalZipWriter::OpenFile(const char* name) + { + std::string p = indexer_.OpenFile(name); + writer_.OpenFile(p.c_str()); + } + + void HierarchicalZipWriter::OpenDirectory(const char* name) + { + indexer_.OpenDirectory(name); + } + + void HierarchicalZipWriter::CloseDirectory() + { + indexer_.CloseDirectory(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/Compression/HierarchicalZipWriter.h Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,123 @@ +/** + * 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 "ZipWriter.h" + +#include <map> +#include <list> +#include <boost/lexical_cast.hpp> + +namespace Orthanc +{ + class HierarchicalZipWriter + { +#if ORTHANC_BUILD_UNIT_TESTS == 1 + FRIEND_TEST(HierarchicalZipWriter, Index); + FRIEND_TEST(HierarchicalZipWriter, Filenames); +#endif + + private: + class Index + { + private: + struct Directory + { + typedef std::map<std::string, unsigned int> Content; + + std::string name_; + Content content_; + }; + + typedef std::list<Directory*> Stack; + + Stack stack_; + + std::string GetCurrentDirectoryPath() const; + + std::string EnsureUniqueFilename(const char* filename); + + public: + Index(); + + ~Index(); + + bool IsRoot() const + { + return stack_.size() == 1; + } + + std::string OpenFile(const char* name); + + void OpenDirectory(const char* name); + + void CloseDirectory(); + + static std::string KeepAlphanumeric(const std::string& source); + }; + + Index indexer_; + ZipWriter writer_; + + public: + HierarchicalZipWriter(const char* path); + + ~HierarchicalZipWriter(); + + void SetCompressionLevel(uint8_t level) + { + writer_.SetCompressionLevel(level); + } + + uint8_t GetCompressionLevel() const + { + return writer_.GetCompressionLevel(); + } + + void OpenFile(const char* name); + + void OpenDirectory(const char* name); + + void CloseDirectory(); + + void Write(const char* data, size_t length) + { + writer_.Write(data, length); + } + + void Write(const std::string& data) + { + writer_.Write(data); + } + }; +}
--- a/Core/Compression/ZipWriter.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/Compression/ZipWriter.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -1,3 +1,35 @@ +/** + * 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 "ZipWriter.h" #include "../../Resources/minizip/zip.h" @@ -104,7 +136,7 @@ compressionLevel_ = level; } - void ZipWriter::CreateFileInZip(const char* path) + void ZipWriter::OpenFile(const char* path) { Open(); @@ -139,7 +171,7 @@ { if (!hasFileInZip_) { - throw OrthancException("Call first CreateFileInZip()"); + throw OrthancException("Call first OpenFile()"); } const size_t maxBytesInAStep = std::numeric_limits<int32_t>::max();
--- a/Core/Compression/ZipWriter.h Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/Compression/ZipWriter.h Fri Dec 14 11:24:24 2012 +0100 @@ -1,9 +1,45 @@ +/** + * 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 <stdint.h> #include <string> #include <boost/shared_ptr.hpp> +#if ORTHANC_BUILD_UNIT_TESTS == 1 +#include <gtest/gtest_prod.h> +#endif + namespace Orthanc { class ZipWriter @@ -41,7 +77,7 @@ return path_; } - void CreateFileInZip(const char* path); + void OpenFile(const char* path); void Write(const char* data, size_t length);
--- a/Core/Compression/ZlibCompressor.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/Compression/ZlibCompressor.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -98,7 +110,15 @@ size_t uncompressedLength; memcpy(&uncompressedLength, compressed, sizeof(size_t)); - uncompressed.resize(uncompressedLength); + + try + { + uncompressed.resize(uncompressedLength); + } + catch (...) + { + throw OrthancException("Zlib: Corrupted compressed buffer"); + } uLongf tmp = uncompressedLength; int error = uncompress
--- a/Core/Compression/ZlibCompressor.h Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/Compression/ZlibCompressor.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/Core/DicomFormat/DicomArray.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/DicomFormat/DicomArray.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/Core/DicomFormat/DicomArray.h Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/DicomFormat/DicomArray.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/Core/DicomFormat/DicomElement.h Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/DicomFormat/DicomElement.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomFormat/DicomInstanceHasher.cpp Fri Dec 14 11:24:24 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 "DicomInstanceHasher.h" + +#include "../OrthancException.h" +#include "../Toolbox.h" + +namespace Orthanc +{ + DicomInstanceHasher::DicomInstanceHasher(const DicomMap& instance) + { + patientId_ = instance.GetValue(DICOM_TAG_PATIENT_ID).AsString(); + studyUid_ = instance.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString(); + seriesUid_ = instance.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).AsString(); + instanceUid_ = instance.GetValue(DICOM_TAG_SOP_INSTANCE_UID).AsString(); + + if (patientId_.size() == 0 || + studyUid_.size() == 0 || + seriesUid_.size() == 0 || + instanceUid_.size() == 0) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + + const std::string& DicomInstanceHasher::HashPatient() + { + if (patientHash_.size() == 0) + { + Toolbox::ComputeSHA1(patientHash_, patientId_); + } + + return patientHash_; + } + + const std::string& DicomInstanceHasher::HashStudy() + { + if (studyHash_.size() == 0) + { + Toolbox::ComputeSHA1(studyHash_, patientId_ + "|" + studyUid_); + } + + return studyHash_; + } + + const std::string& DicomInstanceHasher::HashSeries() + { + if (seriesHash_.size() == 0) + { + Toolbox::ComputeSHA1(seriesHash_, patientId_ + "|" + studyUid_ + "|" + seriesUid_); + } + + return seriesHash_; + } + + const std::string& DicomInstanceHasher::HashInstance() + { + if (instanceHash_.size() == 0) + { + Toolbox::ComputeSHA1(instanceHash_, patientId_ + "|" + studyUid_ + "|" + seriesUid_ + "|" + instanceUid_); + } + + return instanceHash_; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomFormat/DicomInstanceHasher.h Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,93 @@ +/** + * 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 "DicomMap.h" + +namespace Orthanc +{ + /** + * This class implements the hashing mechanism that is used to + * convert DICOM unique identifiers to Orthanc identifiers. Any + * Orthanc identifier for a DICOM resource corresponds to the SHA-1 + * hash of the DICOM identifiers. + + * \note SHA-1 hash is used because it is less sensitive to + * collision attacks than MD5. <a + * href="http://en.wikipedia.org/wiki/SHA-256#Comparison_of_SHA_functions">[Reference]</a> + **/ + class DicomInstanceHasher + { + private: + std::string patientId_; + std::string studyUid_; + std::string seriesUid_; + std::string instanceUid_; + + std::string patientHash_; + std::string studyHash_; + std::string seriesHash_; + std::string instanceHash_; + + public: + DicomInstanceHasher(const DicomMap& instance); + + const std::string& GetPatientId() const + { + return patientId_; + } + + const std::string& GetStudyUid() const + { + return studyUid_; + } + + const std::string& GetSeriesUid() const + { + return seriesUid_; + } + + const std::string& GetInstanceUid() const + { + return instanceUid_; + } + + const std::string& HashPatient(); + + const std::string& HashStudy(); + + const std::string& HashSeries(); + + const std::string& HashInstance(); + }; +}
--- a/Core/DicomFormat/DicomIntegerPixelAccessor.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/DicomFormat/DicomIntegerPixelAccessor.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/Core/DicomFormat/DicomIntegerPixelAccessor.h Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/DicomFormat/DicomIntegerPixelAccessor.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/Core/DicomFormat/DicomMap.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/DicomFormat/DicomMap.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/Core/DicomFormat/DicomMap.h Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/DicomFormat/DicomMap.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/Core/DicomFormat/DicomNullValue.h Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/DicomFormat/DicomNullValue.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/Core/DicomFormat/DicomString.h Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/DicomFormat/DicomString.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/Core/DicomFormat/DicomTag.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/DicomFormat/DicomTag.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/Core/DicomFormat/DicomTag.h Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/DicomFormat/DicomTag.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/Core/DicomFormat/DicomValue.h Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/DicomFormat/DicomValue.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -20,18 +32,15 @@ #pragma once -#include <boost/noncopyable.hpp> +#include "../IDynamicObject.h" + #include <string> namespace Orthanc { - class DicomValue : public boost::noncopyable + class DicomValue : public IDynamicObject { public: - virtual ~DicomValue() - { - } - virtual DicomValue* Clone() const = 0; virtual std::string AsString() const = 0;
--- a/Core/Enumerations.h Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/Enumerations.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -34,12 +46,18 @@ ErrorCode_ParameterOutOfRange, ErrorCode_NotEnoughMemory, ErrorCode_BadParameterType, + ErrorCode_BadSequenceOfCalls, + ErrorCode_InexistentItem, // Specific error codes ErrorCode_UriSyntax, ErrorCode_InexistentFile, ErrorCode_CannotWriteFile, - ErrorCode_BadFileFormat + ErrorCode_BadFileFormat, + ErrorCode_Timeout, + ErrorCode_UnknownResource, + ErrorCode_IncompatibleDatabaseVersion, + ErrorCode_FullStorage }; enum PixelFormat @@ -48,4 +66,23 @@ PixelFormat_Grayscale8, PixelFormat_Grayscale16 }; + + + /** + * WARNING: Do not change the explicit values in the enumerations + * below this point. This would result in incompatible databases + * between versions of Orthanc! + **/ + + enum CompressionType + { + CompressionType_None = 1, + CompressionType_Zlib = 2 + }; + + enum FileContentType + { + FileContentType_Dicom = 1, + FileContentType_Json = 2 + }; }
--- a/Core/FileStorage.cpp Thu Oct 04 15:09:56 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,294 +0,0 @@ -/** - * 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. - * - * 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 "FileStorage.h" - -// http://stackoverflow.com/questions/1576272/storing-large-number-of-files-in-file-system -// http://stackoverflow.com/questions/446358/storing-a-large-number-of-images - -#include "OrthancException.h" -#include "Toolbox.h" -#include "Uuid.h" - -#include <boost/filesystem/fstream.hpp> - -static std::string ToString(const boost::filesystem::path& p) -{ -#if BOOST_HAS_FILESYSTEM_V3 == 1 - return p.filename().string(); -#else - return p.filename(); -#endif -} - - -namespace Orthanc -{ - boost::filesystem::path FileStorage::GetPath(const std::string& uuid) const - { - namespace fs = boost::filesystem; - - if (!Toolbox::IsUuid(uuid)) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - fs::path path = root_; - - path /= std::string(&uuid[0], &uuid[2]); - path /= std::string(&uuid[2], &uuid[4]); - path /= uuid; - -#if BOOST_HAS_FILESYSTEM_V3 == 1 - path.make_preferred(); -#endif - - return path; - } - - FileStorage::FileStorage(std::string root) - { - namespace fs = boost::filesystem; - - //root_ = boost::filesystem::absolute(root).string(); - root_ = root; - - if (fs::exists(root)) - { - if (!fs::is_directory(root)) - { - throw OrthancException("The file storage root directory is a file"); - } - } - else - { - if (!fs::create_directories(root)) - { - throw OrthancException("Unable to create the file storage root directory"); - } - } - } - - std::string FileStorage::CreateFileWithoutCompression(const void* content, size_t size) - { - std::string uuid; - boost::filesystem::path path; - - for (;;) - { - uuid = Toolbox::GenerateUuid(); - path = GetPath(uuid); - - if (!boost::filesystem::exists(path)) - { - // OK, this is indeed a new file - break; - } - - // Extremely improbable case: This Uuid has already been created - // in the past. Try again. - } - - if (boost::filesystem::exists(path.parent_path())) - { - if (!boost::filesystem::is_directory(path.parent_path())) - { - throw OrthancException("The subdirectory to be created is already occupied by a regular file"); - } - } - else - { - if (!boost::filesystem::create_directories(path.parent_path())) - { - throw OrthancException("Unable to create a subdirectory in the file storage"); - } - } - - boost::filesystem::ofstream f; - f.open(path, std::ofstream::out | std::ios::binary); - if (!f.good()) - { - throw OrthancException("Unable to create a new file in the file storage"); - } - - if (size != 0) - { - f.write(static_cast<const char*>(content), size); - if (!f.good()) - { - f.close(); - throw OrthancException("Unable to write to the new file in the file storage"); - } - } - - f.close(); - - return uuid; - } - - - std::string FileStorage::Create(const void* content, size_t size) - { - if (!HasBufferCompressor() || size == 0) - { - return CreateFileWithoutCompression(content, size); - } - else - { - std::string compressed; - compressor_->Compress(compressed, content, size); - assert(compressed.size() > 0); - return CreateFileWithoutCompression(&compressed[0], compressed.size()); - } - } - - - std::string FileStorage::Create(const std::vector<uint8_t>& content) - { - if (content.size() == 0) - return Create(NULL, 0); - else - return Create(&content[0], content.size()); - } - - std::string FileStorage::Create(const std::string& content) - { - if (content.size() == 0) - return Create(NULL, 0); - else - return Create(&content[0], content.size()); - } - - void FileStorage::ReadFile(std::string& content, - const std::string& uuid) const - { - content.clear(); - - if (HasBufferCompressor()) - { - std::string compressed; - Toolbox::ReadFile(compressed, ToString(GetPath(uuid))); - - if (compressed.size() != 0) - { - compressor_->Uncompress(content, compressed); - } - } - else - { - Toolbox::ReadFile(content, GetPath(uuid).string()); - } - } - - - uintmax_t FileStorage::GetCompressedSize(const std::string& uuid) const - { - boost::filesystem::path path = GetPath(uuid); - return boost::filesystem::file_size(path); - } - - - - void FileStorage::ListAllFiles(std::set<std::string>& result) const - { - namespace fs = boost::filesystem; - - result.clear(); - - if (fs::exists(root_) && fs::is_directory(root_)) - { - for (fs::recursive_directory_iterator current(root_), end; current != end ; ++current) - { - if (fs::is_regular_file(current->status())) - { - try - { - fs::path d = current->path(); - std::string uuid = ToString(d); - if (Toolbox::IsUuid(uuid)) - { - fs::path p0 = d.parent_path().parent_path().parent_path(); - std::string p1 = ToString(d.parent_path().parent_path()); - std::string p2 = ToString(d.parent_path()); - if (p1.length() == 2 && - p2.length() == 2 && - p1 == uuid.substr(0, 2) && - p2 == uuid.substr(2, 2) && - p0 == root_) - { - result.insert(uuid); - } - } - } - catch (fs::filesystem_error) - { - } - } - } - } - } - - - void FileStorage::Clear() - { - namespace fs = boost::filesystem; - typedef std::set<std::string> List; - - List result; - ListAllFiles(result); - - for (List::const_iterator it = result.begin(); it != result.end(); it++) - { - Remove(*it); - } - } - - - void FileStorage::Remove(const std::string& uuid) - { - namespace fs = boost::filesystem; - - fs::path p = GetPath(uuid); - fs::remove(p); - - // Remove the two parent directories, ignoring the error code if - // these directories are not empty - -#if BOOST_HAS_FILESYSTEM_V3 == 1 - boost::system::error_code err; - fs::remove(p.parent_path(), err); - fs::remove(p.parent_path().parent_path(), err); -#else - fs::remove(p.parent_path()); - fs::remove(p.parent_path().parent_path()); -#endif - } - - - uintmax_t FileStorage::GetCapacity() const - { - return boost::filesystem::space(root_).capacity; - } - - uintmax_t FileStorage::GetAvailableSpace() const - { - return boost::filesystem::space(root_).available; - } -}
--- a/Core/FileStorage.h Thu Oct 04 15:09:56 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,82 +0,0 @@ -/** - * 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. - * - * 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 <boost/filesystem.hpp> -#include <set> - -#include "Compression/BufferCompressor.h" - -namespace Orthanc -{ - class FileStorage : public boost::noncopyable - { - friend class HttpOutput; - - private: - std::auto_ptr<BufferCompressor> compressor_; - - boost::filesystem::path root_; - - boost::filesystem::path GetPath(const std::string& uuid) const; - - std::string CreateFileWithoutCompression(const void* content, size_t size); - - public: - FileStorage(std::string root); - - void SetBufferCompressor(BufferCompressor* compressor) // Takes the ownership - { - compressor_.reset(compressor); - } - - bool HasBufferCompressor() const - { - return compressor_.get() != NULL; - } - - std::string Create(const void* content, size_t size); - - std::string Create(const std::vector<uint8_t>& content); - - std::string Create(const std::string& content); - - void ReadFile(std::string& content, - const std::string& uuid) const; - - void ListAllFiles(std::set<std::string>& result) const; - - uintmax_t GetCompressedSize(const std::string& uuid) const; - - void Clear(); - - void Remove(const std::string& uuid); - - uintmax_t GetCapacity() const; - - uintmax_t GetAvailableSpace() const; - - std::string GetPath() const - { - return root_.string(); - } - }; -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/FileStorage/CompressedFileStorageAccessor.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,120 @@ +/** + * 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 "CompressedFileStorageAccessor.h" + +#include "../OrthancException.h" +#include "FileStorageAccessor.h" +#include "../HttpServer/BufferHttpSender.h" + +namespace Orthanc +{ + FileInfo CompressedFileStorageAccessor::WriteInternal(const void* data, + size_t size, + FileContentType type) + { + switch (compressionType_) + { + case CompressionType_None: + { + std::string uuid = storage_.Create(data, size); + return FileInfo(uuid, type, size); + } + + case CompressionType_Zlib: + { + std::string compressed; + zlib_.Compress(compressed, data, size); + std::string uuid = storage_.Create(compressed); + return FileInfo(uuid, type, size, + CompressionType_Zlib, compressed.size()); + } + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + CompressedFileStorageAccessor::CompressedFileStorageAccessor(FileStorage& storage) : + storage_(storage) + { + compressionType_ = CompressionType_None; + } + + void CompressedFileStorageAccessor::Read(std::string& content, + const std::string& uuid) + { + switch (compressionType_) + { + case CompressionType_None: + storage_.ReadFile(content, uuid); + break; + + case CompressionType_Zlib: + { + std::string compressed; + storage_.ReadFile(compressed, uuid); + zlib_.Uncompress(content, compressed); + break; + } + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + HttpFileSender* CompressedFileStorageAccessor::ConstructHttpFileSender(const std::string& uuid) + { + switch (compressionType_) + { + case CompressionType_None: + { + FileStorageAccessor uncompressedAccessor(storage_); + return uncompressedAccessor.ConstructHttpFileSender(uuid); + } + + case CompressionType_Zlib: + { + std::string compressed; + storage_.ReadFile(compressed, uuid); + + std::auto_ptr<BufferHttpSender> sender(new BufferHttpSender); + zlib_.Uncompress(sender->GetBuffer(), compressed); + + return sender.release(); + } + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/FileStorage/CompressedFileStorageAccessor.h Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,71 @@ +/** + * 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 "StorageAccessor.h" +#include "FileStorage.h" +#include "../Compression/ZlibCompressor.h" + +namespace Orthanc +{ + class CompressedFileStorageAccessor : public StorageAccessor + { + private: + FileStorage& storage_; + ZlibCompressor zlib_; + CompressionType compressionType_; + + protected: + virtual FileInfo WriteInternal(const void* data, + size_t size, + FileContentType type); + + public: + CompressedFileStorageAccessor(FileStorage& storage); + + void SetCompressionForNextOperations(CompressionType compression) + { + compressionType_ = compression; + } + + CompressionType GetCompressionForNextOperations() + { + return compressionType_; + } + + virtual void Read(std::string& content, + const std::string& uuid); + + virtual HttpFileSender* ConstructHttpFileSender(const std::string& uuid); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/FileStorage/FileInfo.h Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,110 @@ +/** + * 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 <stdint.h> +#include "../Enumerations.h" + +namespace Orthanc +{ + struct FileInfo + { + private: + std::string uuid_; + FileContentType contentType_; + uint64_t uncompressedSize_; + CompressionType compressionType_; + uint64_t compressedSize_; + + public: + FileInfo() + { + } + + /** + * Constructor for an uncompressed attachment. + **/ + FileInfo(const std::string& uuid, + FileContentType contentType, + uint64_t size) : + uuid_(uuid), + contentType_(contentType), + uncompressedSize_(size), + compressionType_(CompressionType_None), + compressedSize_(size) + { + } + + /** + * Constructor for a compressed attachment. + **/ + FileInfo(const std::string& uuid, + FileContentType contentType, + uint64_t uncompressedSize, + CompressionType compressionType, + uint64_t compressedSize) : + uuid_(uuid), + contentType_(contentType), + uncompressedSize_(uncompressedSize), + compressionType_(compressionType), + compressedSize_(compressedSize) + { + } + + const std::string& GetUuid() const + { + return uuid_; + } + + FileContentType GetContentType() const + { + return contentType_; + } + + uint64_t GetUncompressedSize() const + { + return uncompressedSize_; + } + + CompressionType GetCompressionType() const + { + return compressionType_; + } + + uint64_t GetCompressedSize() const + { + return compressedSize_; + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/FileStorage/FileStorage.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,323 @@ +/** + * 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 "FileStorage.h" + +// http://stackoverflow.com/questions/1576272/storing-large-number-of-files-in-file-system +// http://stackoverflow.com/questions/446358/storing-a-large-number-of-images + +#include "../OrthancException.h" +#include "../Toolbox.h" +#include "../Uuid.h" + +#include <boost/filesystem/fstream.hpp> +#include <glog/logging.h> + +static std::string ToString(const boost::filesystem::path& p) +{ +#if BOOST_HAS_FILESYSTEM_V3 == 1 + return p.filename().string(); +#else + return p.filename(); +#endif +} + + +namespace Orthanc +{ + boost::filesystem::path FileStorage::GetPath(const std::string& uuid) const + { + namespace fs = boost::filesystem; + + if (!Toolbox::IsUuid(uuid)) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + fs::path path = root_; + + path /= std::string(&uuid[0], &uuid[2]); + path /= std::string(&uuid[2], &uuid[4]); + path /= uuid; + +#if BOOST_HAS_FILESYSTEM_V3 == 1 + path.make_preferred(); +#endif + + return path; + } + + FileStorage::FileStorage(std::string root) + { + namespace fs = boost::filesystem; + + //root_ = boost::filesystem::absolute(root).string(); + root_ = root; + + if (fs::exists(root)) + { + if (!fs::is_directory(root)) + { + throw OrthancException("The file storage root directory is a file"); + } + } + else + { + if (!fs::create_directories(root)) + { + throw OrthancException("Unable to create the file storage root directory"); + } + } + } + + std::string FileStorage::CreateFileWithoutCompression(const void* content, size_t size) + { + std::string uuid; + boost::filesystem::path path; + + for (;;) + { + uuid = Toolbox::GenerateUuid(); + path = GetPath(uuid); + + if (!boost::filesystem::exists(path)) + { + // OK, this is indeed a new file + break; + } + + // Extremely improbable case: This Uuid has already been created + // in the past. Try again. + } + + if (boost::filesystem::exists(path.parent_path())) + { + if (!boost::filesystem::is_directory(path.parent_path())) + { + throw OrthancException("The subdirectory to be created is already occupied by a regular file"); + } + } + else + { + if (!boost::filesystem::create_directories(path.parent_path())) + { + throw OrthancException("Unable to create a subdirectory in the file storage"); + } + } + + boost::filesystem::ofstream f; + f.open(path, std::ofstream::out | std::ios::binary); + if (!f.good()) + { + throw OrthancException("Unable to create a new file in the file storage"); + } + + if (size != 0) + { + f.write(static_cast<const char*>(content), size); + if (!f.good()) + { + f.close(); + throw OrthancException("Unable to write to the new file in the file storage"); + } + } + + f.close(); + + return uuid; + } + + + std::string FileStorage::Create(const void* content, size_t size) + { + if (!HasBufferCompressor() || size == 0) + { + return CreateFileWithoutCompression(content, size); + } + else + { + std::string compressed; + compressor_->Compress(compressed, content, size); + assert(compressed.size() > 0); + return CreateFileWithoutCompression(&compressed[0], compressed.size()); + } + } + + + std::string FileStorage::Create(const std::vector<uint8_t>& content) + { + if (content.size() == 0) + return Create(NULL, 0); + else + return Create(&content[0], content.size()); + } + + std::string FileStorage::Create(const std::string& content) + { + if (content.size() == 0) + return Create(NULL, 0); + else + return Create(&content[0], content.size()); + } + + void FileStorage::ReadFile(std::string& content, + const std::string& uuid) const + { + content.clear(); + + if (HasBufferCompressor()) + { + std::string compressed; + Toolbox::ReadFile(compressed, ToString(GetPath(uuid))); + + if (compressed.size() != 0) + { + compressor_->Uncompress(content, compressed); + } + } + else + { + Toolbox::ReadFile(content, GetPath(uuid).string()); + } + } + + + uintmax_t FileStorage::GetCompressedSize(const std::string& uuid) const + { + boost::filesystem::path path = GetPath(uuid); + return boost::filesystem::file_size(path); + } + + + + void FileStorage::ListAllFiles(std::set<std::string>& result) const + { + namespace fs = boost::filesystem; + + result.clear(); + + if (fs::exists(root_) && fs::is_directory(root_)) + { + for (fs::recursive_directory_iterator current(root_), end; current != end ; ++current) + { + if (fs::is_regular_file(current->status())) + { + try + { + fs::path d = current->path(); + std::string uuid = ToString(d); + if (Toolbox::IsUuid(uuid)) + { + fs::path p0 = d.parent_path().parent_path().parent_path(); + std::string p1 = ToString(d.parent_path().parent_path()); + std::string p2 = ToString(d.parent_path()); + if (p1.length() == 2 && + p2.length() == 2 && + p1 == uuid.substr(0, 2) && + p2 == uuid.substr(2, 2) && + p0 == root_) + { + result.insert(uuid); + } + } + } + catch (fs::filesystem_error) + { + } + } + } + } + } + + + void FileStorage::Clear() + { + namespace fs = boost::filesystem; + typedef std::set<std::string> List; + + List result; + ListAllFiles(result); + + for (List::const_iterator it = result.begin(); it != result.end(); it++) + { + Remove(*it); + } + } + + + void FileStorage::Remove(const std::string& uuid) + { + LOG(INFO) << "Deleting file " << uuid; + namespace fs = boost::filesystem; + + fs::path p = GetPath(uuid); + + try + { + fs::remove(p); + } + catch (...) + { + // Ignore the error + } + + // Remove the two parent directories, ignoring the error code if + // these directories are not empty + + try + { +#if BOOST_HAS_FILESYSTEM_V3 == 1 + boost::system::error_code err; + fs::remove(p.parent_path(), err); + fs::remove(p.parent_path().parent_path(), err); +#else + fs::remove(p.parent_path()); + fs::remove(p.parent_path().parent_path()); +#endif + } + catch (...) + { + // Ignore the error + } + } + + + uintmax_t FileStorage::GetCapacity() const + { + return boost::filesystem::space(root_).capacity; + } + + uintmax_t FileStorage::GetAvailableSpace() const + { + return boost::filesystem::space(root_).available; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/FileStorage/FileStorage.h Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,96 @@ +/** + * 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 <boost/filesystem.hpp> +#include <set> + +#include "../Compression/BufferCompressor.h" + +namespace Orthanc +{ + class FileStorage : public boost::noncopyable + { + // TODO REMOVE THIS + friend class FilesystemHttpSender; + friend class FileStorageAccessor; + + private: + std::auto_ptr<BufferCompressor> compressor_; + + boost::filesystem::path root_; + + boost::filesystem::path GetPath(const std::string& uuid) const; + + std::string CreateFileWithoutCompression(const void* content, size_t size); + + public: + FileStorage(std::string root); + + void SetBufferCompressor(BufferCompressor* compressor) // Takes the ownership + { + compressor_.reset(compressor); + } + + bool HasBufferCompressor() const + { + return compressor_.get() != NULL; + } + + std::string Create(const void* content, size_t size); + + std::string Create(const std::vector<uint8_t>& content); + + std::string Create(const std::string& content); + + void ReadFile(std::string& content, + const std::string& uuid) const; + + void ListAllFiles(std::set<std::string>& result) const; + + uintmax_t GetCompressedSize(const std::string& uuid) const; + + void Clear(); + + void Remove(const std::string& uuid); + + uintmax_t GetCapacity() const; + + uintmax_t GetAvailableSpace() const; + + std::string GetPath() const + { + return root_.string(); + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/FileStorage/FileStorageAccessor.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,43 @@ +/** + * 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 "FileStorageAccessor.h" + +namespace Orthanc +{ + FileInfo FileStorageAccessor::WriteInternal(const void* data, + size_t size, + FileContentType type) + { + return FileInfo(storage_.Create(data, size), type, size); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/FileStorage/FileStorageAccessor.h Fri Dec 14 11:24:24 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 "StorageAccessor.h" +#include "FileStorage.h" +#include "../HttpServer/FilesystemHttpSender.h" + +namespace Orthanc +{ + class FileStorageAccessor : public StorageAccessor + { + private: + FileStorage& storage_; + + protected: + virtual FileInfo WriteInternal(const void* data, + size_t size, + FileContentType type); + + public: + FileStorageAccessor(FileStorage& storage) : storage_(storage) + { + } + + virtual void Read(std::string& content, + const std::string& uuid) + { + storage_.ReadFile(content, uuid); + } + + virtual HttpFileSender* ConstructHttpFileSender(const std::string& uuid) + { + return new FilesystemHttpSender(storage_.GetPath(uuid)); + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/FileStorage/StorageAccessor.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,62 @@ +/** + * 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 "StorageAccessor.h" + +namespace Orthanc +{ + FileInfo StorageAccessor::Write(const std::vector<uint8_t>& content, + FileContentType type) + { + if (content.size() == 0) + { + return WriteInternal(NULL, 0, type); + } + else + { + return WriteInternal(&content[0], content.size(), type); + } + } + + FileInfo StorageAccessor::Write(const std::string& content, + FileContentType type) + { + if (content.size() == 0) + { + return WriteInternal(NULL, 0, type); + } + else + { + return WriteInternal(&content[0], content.size(), type); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/FileStorage/StorageAccessor.h Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,75 @@ +/** + * 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 "FileInfo.h" +#include "../HttpServer/HttpFileSender.h" + +#include <vector> +#include <string> +#include <boost/noncopyable.hpp> +#include <stdint.h> + +namespace Orthanc +{ + class StorageAccessor : boost::noncopyable + { + protected: + virtual FileInfo WriteInternal(const void* data, + size_t size, + FileContentType type) = 0; + + public: + virtual ~StorageAccessor() + { + } + + FileInfo Write(const void* data, + size_t size, + FileContentType type) + { + return WriteInternal(data, size, type); + } + + FileInfo Write(const std::vector<uint8_t>& content, + FileContentType type); + + FileInfo Write(const std::string& content, + FileContentType type); + + virtual void Read(std::string& content, + const std::string& uuid) = 0; + + virtual HttpFileSender* ConstructHttpFileSender(const std::string& uuid) = 0; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/HttpServer/BufferHttpSender.h Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,68 @@ +/** + * 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 "HttpFileSender.h" + +namespace Orthanc +{ + class BufferHttpSender : public HttpFileSender + { + private: + std::string buffer_; + + protected: + virtual uint64_t GetFileSize() + { + return buffer_.size(); + } + + virtual bool SendData(HttpOutput& output) + { + if (buffer_.size()) + output.Send(&buffer_[0], buffer_.size()); + + return true; + } + + public: + std::string& GetBuffer() + { + return buffer_; + } + + const std::string& GetBuffer() const + { + return buffer_; + } + }; +}
--- a/Core/HttpServer/EmbeddedResourceHttpHandler.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/HttpServer/EmbeddedResourceHttpHandler.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -23,6 +35,7 @@ #include "../OrthancException.h" #include <stdio.h> +#include <glog/logging.h> namespace Orthanc @@ -67,6 +80,7 @@ } catch (OrthancException& e) { + LOG(WARNING) << "Unable to find HTTP resource: " << resourcePath; output.SendHeader(Orthanc_HttpStatus_404_NotFound); } }
--- a/Core/HttpServer/EmbeddedResourceHttpHandler.h Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/HttpServer/EmbeddedResourceHttpHandler.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/Core/HttpServer/FilesystemHttpHandler.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/HttpServer/FilesystemHttpHandler.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -21,6 +33,7 @@ #include "FilesystemHttpHandler.h" #include "../OrthancException.h" +#include "FilesystemHttpSender.h" #include <boost/filesystem.hpp> @@ -41,7 +54,7 @@ { namespace fs = boost::filesystem; - output.SendOkHeader("text/html"); + output.SendCustomOkHeader("Content-Type: text/html\r\n"); output.SendString("<html>"); output.SendString(" <body>"); output.SendString(" <h1>Subdirectories</h1>"); @@ -136,7 +149,9 @@ if (fs::exists(p) && fs::is_regular_file(p)) { - output.AnswerFileAutodetectContentType(p.string()); + FilesystemHttpSender(p).Send(output); + + //output.AnswerFileAutodetectContentType(p.string()); } else if (listDirectoryContent_ && fs::exists(p) &&
--- a/Core/HttpServer/FilesystemHttpHandler.h Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/HttpServer/FilesystemHttpHandler.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/HttpServer/FilesystemHttpSender.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,102 @@ +/** + * 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 "FilesystemHttpSender.h" + +#include "../Toolbox.h" + +#include <stdio.h> + +namespace Orthanc +{ + void FilesystemHttpSender::Setup() + { + //SetDownloadFilename(path_.filename().string()); + +#if BOOST_HAS_FILESYSTEM_V3 == 1 + SetContentType(Toolbox::AutodetectMimeType(path_.filename().string())); +#else + SetContentType(Toolbox::AutodetectMimeType(path_.filename())); +#endif + } + + uint64_t FilesystemHttpSender::GetFileSize() + { + return Toolbox::GetFileSize(path_.string()); + } + + bool FilesystemHttpSender::SendData(HttpOutput& output) + { + FILE* fp = fopen(path_.string().c_str(), "rb"); + if (!fp) + { + return false; + } + + std::vector<uint8_t> buffer(1024 * 1024); // Chunks of 1MB + + for (;;) + { + size_t nbytes = fread(&buffer[0], 1, buffer.size(), fp); + if (nbytes == 0) + { + break; + } + else + { + output.Send(&buffer[0], nbytes); + } + } + + fclose(fp); + + return true; + } + + FilesystemHttpSender::FilesystemHttpSender(const char* path) + { + path_ = std::string(path); + Setup(); + } + + FilesystemHttpSender::FilesystemHttpSender(const boost::filesystem::path& path) + { + path_ = path; + Setup(); + } + + FilesystemHttpSender::FilesystemHttpSender(const FileStorage& storage, + const std::string& uuid) + { + path_ = storage.GetPath(uuid).string(); + Setup(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/HttpServer/FilesystemHttpSender.h Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,59 @@ +/** + * 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 "HttpFileSender.h" +#include "../FileStorage/FileStorage.h" + +namespace Orthanc +{ + class FilesystemHttpSender : public HttpFileSender + { + private: + boost::filesystem::path path_; + + void Setup(); + + protected: + virtual uint64_t GetFileSize(); + + virtual bool SendData(HttpOutput& output); + + public: + FilesystemHttpSender(const char* path); + + FilesystemHttpSender(const boost::filesystem::path& path); + + FilesystemHttpSender(const FileStorage& storage, + const std::string& uuid); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/HttpServer/HttpFileSender.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,66 @@ +/** + * 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 "HttpFileSender.h" + +#include <boost/lexical_cast.hpp> + +namespace Orthanc +{ + void HttpFileSender::SendHeader(HttpOutput& output) + { + std::string header; + header += "Content-Length: " + boost::lexical_cast<std::string>(GetFileSize()) + "\r\n"; + + if (contentType_.size() > 0) + { + header += "Content-Type: " + contentType_ + "\r\n"; + } + + if (downloadFilename_.size() > 0) + { + header += "Content-Disposition: attachment; filename=\"" + downloadFilename_ + "\"\r\n"; + } + + output.SendCustomOkHeader(header); + } + + void HttpFileSender::Send(HttpOutput& output) + { + SendHeader(output); + + if (!SendData(output)) + { + output.SendHeader(Orthanc_HttpStatus_500_InternalServerError); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/HttpServer/HttpFileSender.h Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,89 @@ +/** + * 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 "HttpOutput.h" + +namespace Orthanc +{ + class HttpFileSender + { + private: + std::string contentType_; + std::string downloadFilename_; + + void SendHeader(HttpOutput& output); + + protected: + virtual uint64_t GetFileSize() = 0; + + virtual bool SendData(HttpOutput& output) = 0; + + public: + virtual ~HttpFileSender() + { + } + + void ResetContentType() + { + contentType_.clear(); + } + + void SetContentType(const std::string& contentType) + { + contentType_ = contentType; + } + + const std::string& GetContentType() const + { + return contentType_; + } + + void ResetDownloadFilename() + { + downloadFilename_.clear(); + } + + void SetDownloadFilename(const std::string& filename) + { + downloadFilename_ = filename; + } + + const std::string& GetDownloadFilename() const + { + return downloadFilename_; + } + + void Send(HttpOutput& output); + }; +}
--- a/Core/HttpServer/HttpHandler.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/HttpServer/HttpHandler.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -64,12 +76,12 @@ - std::string HttpHandler::GetArgument(const Arguments& arguments, + std::string HttpHandler::GetArgument(const Arguments& getArguments, const std::string& name, const std::string& defaultValue) { - Arguments::const_iterator it = arguments.find(name); - if (it == arguments.end()) + Arguments::const_iterator it = getArguments.find(name); + if (it == getArguments.end()) { return defaultValue; }
--- a/Core/HttpServer/HttpHandler.h Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/HttpServer/HttpHandler.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -43,13 +55,13 @@ const std::string& method, const UriComponents& uri, const Arguments& headers, - const Arguments& arguments, + const Arguments& getArguments, const std::string& postData) = 0; static void ParseGetQuery(HttpHandler::Arguments& result, const char* query); - static std::string GetArgument(const Arguments& arguments, + static std::string GetArgument(const Arguments& getArguments, const std::string& name, const std::string& defaultValue); };
--- a/Core/HttpServer/HttpOutput.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/HttpServer/HttpOutput.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -32,38 +44,19 @@ void HttpOutput::SendString(const std::string& s) { if (s.size() > 0) + { Send(&s[0], s.size()); - } - - void HttpOutput::SendOkHeader(const std::string& contentType) - { - SendOkHeader(contentType.c_str(), false, 0); - } - - void HttpOutput::SendOkHeader() - { - SendOkHeader(NULL, false, 0); + } } - void HttpOutput::SendOkHeader(uint64_t contentLength) - { - SendOkHeader(NULL, true, contentLength); - } - - void HttpOutput::SendOkHeader(const std::string& contentType, - uint64_t contentLength) - { - SendOkHeader(contentType.c_str(), true, contentLength); - } - - void HttpOutput::SendOkHeader(const char* contentType, bool hasContentLength, - uint64_t contentLength) + uint64_t contentLength, + const char* contentFilename) { std::string s = "HTTP/1.1 200 OK\r\n"; - if (contentType) + if (contentType && contentType[0] != '\0') { s += "Content-Type: " + std::string(contentType) + "\r\n"; } @@ -73,12 +66,24 @@ s += "Content-Length: " + boost::lexical_cast<std::string>(contentLength) + "\r\n"; } + if (contentFilename && contentFilename[0] != '\0') + { + s += "Content-Disposition: attachment; filename=\"" + std::string(contentFilename) + "\"\r\n"; + } + s += "\r\n"; Send(&s[0], s.size()); } + void HttpOutput::SendCustomOkHeader(const std::string& customHeader) + { + std::string s = "HTTP/1.1 200 OK\r\n" + customHeader + "\r\n"; + Send(&s[0], s.size()); + } + + void HttpOutput::SendMethodNotAllowedError(const std::string& allowed) { std::string s = @@ -114,7 +119,7 @@ void HttpOutput::AnswerBufferWithContentType(const std::string& buffer, const std::string& contentType) { - SendOkHeader(contentType.c_str(), true, buffer.size()); + SendOkHeader(contentType.c_str(), true, buffer.size(), NULL); SendString(buffer); } @@ -123,60 +128,10 @@ size_t size, const std::string& contentType) { - SendOkHeader(contentType.c_str(), true, size); + SendOkHeader(contentType.c_str(), true, size, NULL); Send(buffer, size); } - - void HttpOutput::AnswerFileWithContentType(const std::string& path, - const std::string& contentType) - { - uint64_t fileSize = Toolbox::GetFileSize(path); - - FILE* fp = fopen(path.c_str(), "rb"); - if (!fp) - { - SendHeaderInternal(Orthanc_HttpStatus_500_InternalServerError); - return; - } - - SendOkHeader(contentType.c_str(), true, fileSize); - - std::vector<uint8_t> buffer(1024 * 1024); // Chunks of 1MB - - for (;;) - { - size_t nbytes = fread(&buffer[0], 1, buffer.size(), fp); - if (nbytes == 0) - { - break; - } - else - { - Send(&buffer[0], nbytes); - } - } - - fclose(fp); - } - - - void HttpOutput::AnswerFileAutodetectContentType(const std::string& path) - { - AnswerFileWithContentType(path, Toolbox::AutodetectMimeType(path)); - } - - - void HttpOutput::AnswerFile(const FileStorage& storage, - const std::string& uuid, - const std::string& contentType) - { - boost::filesystem::path p(storage.GetPath(uuid)); - AnswerFileWithContentType(p.string(), contentType); - } - - - void HttpOutput::Redirect(const std::string& path) { std::string s =
--- a/Core/HttpServer/HttpOutput.h Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/HttpServer/HttpOutput.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -23,7 +35,6 @@ #include <string> #include <stdint.h> #include "../Enumerations.h" -#include "../FileStorage.h" namespace Orthanc { @@ -32,10 +43,6 @@ private: void SendHeaderInternal(Orthanc_HttpStatus status); - void SendOkHeader(const char* contentType, - bool hasContentLength, - uint64_t contentLength); - public: virtual ~HttpOutput() { @@ -43,28 +50,22 @@ virtual void Send(const void* buffer, size_t length) = 0; - void SendString(const std::string& s); - - void SendOkHeader(); + void SendOkHeader(const char* contentType, + bool hasContentLength, + uint64_t contentLength, + const char* contentFilename); - void SendOkHeader(uint64_t contentLength); + void SendCustomOkHeader(const std::string& customHeader); - void SendOkHeader(const std::string& contentType); - - void SendOkHeader(const std::string& contentType, - uint64_t contentLength); + void SendString(const std::string& s); void SendMethodNotAllowedError(const std::string& allowed); void SendHeader(Orthanc_HttpStatus status); - - // Higher-level constructs to send entire files or buffers ------------------- + void Redirect(const std::string& path); - void AnswerBuffer(const std::string& buffer) - { - AnswerBufferWithContentType(buffer, ""); - } + // Higher-level constructs to send entire buffers ---------------------------- void AnswerBufferWithContentType(const std::string& buffer, const std::string& contentType); @@ -72,27 +73,5 @@ void AnswerBufferWithContentType(const void* buffer, size_t size, const std::string& contentType); - - void AnswerFile(const std::string& path) - { - AnswerFileWithContentType(path, ""); - } - - void AnswerFileWithContentType(const std::string& path, - const std::string& contentType); - - void AnswerFileAutodetectContentType(const std::string& path); - - void AnswerFile(const FileStorage& storage, - const std::string& uuid) - { - AnswerFile(storage, uuid, ""); - } - - void AnswerFile(const FileStorage& storage, - const std::string& uuid, - const std::string& contentType); - - void Redirect(const std::string& path); }; }
--- a/Core/HttpServer/MongooseServer.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/HttpServer/MongooseServer.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -63,7 +75,10 @@ virtual void Send(const void* buffer, size_t length) { - mg_write(connection_, buffer, length); + if (length > 0) + { + mg_write(connection_, buffer, length); + } } }; @@ -477,7 +492,8 @@ { HttpHandler::ParseGetQuery(arguments, request->query_string); } - else if (!strcmp(request->request_method, "POST")) + else if (!strcmp(request->request_method, "POST") || + !strcmp(request->request_method, "PUT")) { HttpHandler::Arguments::const_iterator ct = headers.find("content-type"); if (ct == headers.end()) @@ -510,7 +526,7 @@ return (void*) ""; case PostDataStatus_Pending: - output.AnswerBuffer(""); + output.AnswerBufferWithContentType(NULL, 0, ""); return (void*) ""; default: @@ -573,7 +589,7 @@ } - void MongooseServer::SetPort(uint16_t port) + void MongooseServer::SetPortNumber(uint16_t port) { Stop(); port_ = port;
--- a/Core/HttpServer/MongooseServer.h Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/HttpServer/MongooseServer.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -24,6 +36,7 @@ #include <list> #include <map> +#include <set> #include <stdint.h> #include <boost/shared_ptr.hpp> @@ -57,9 +70,9 @@ ~MongooseServer(); - void SetPort(uint16_t port); + void SetPortNumber(uint16_t port); - uint16_t GetPort() const + uint16_t GetPortNumber() const { return port_; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/IDynamicObject.h Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,52 @@ +/** + * 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 <boost/noncopyable.hpp> + +namespace Orthanc +{ + /** + * This class should be the ancestor to any class whose type is + * determined at the runtime, and that can be dynamically allocated. + * Being a child of IDynamicObject only implies the existence of a + * virtual destructor. + **/ + class IDynamicObject : public boost::noncopyable + { + public: + virtual ~IDynamicObject() + { + } + }; +}
--- a/Core/MultiThreading/BagOfRunnablesBySteps.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/MultiThreading/BagOfRunnablesBySteps.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/Core/MultiThreading/BagOfRunnablesBySteps.h Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/MultiThreading/BagOfRunnablesBySteps.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/Core/MultiThreading/IRunnableBySteps.h Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/MultiThreading/IRunnableBySteps.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/Core/OrthancException.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/OrthancException.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -39,39 +51,57 @@ { switch (error) { - case ErrorCode_Success: - return "Success"; + case ErrorCode_Success: + return "Success"; + + case ErrorCode_ParameterOutOfRange: + return "Parameter out of range"; - case ErrorCode_ParameterOutOfRange: - return "Parameter out of range"; + case ErrorCode_NotImplemented: + return "Not implemented yet"; + + case ErrorCode_InternalError: + return "Internal error"; - case ErrorCode_NotImplemented: - return "Not implemented yet"; + case ErrorCode_NotEnoughMemory: + return "Not enough memory"; + + case ErrorCode_UriSyntax: + return "Badly formatted URI"; - case ErrorCode_InternalError: - return "Internal error"; + case ErrorCode_BadParameterType: + return "Bad type for a parameter"; - case ErrorCode_NotEnoughMemory: - return "Not enough memory"; + case ErrorCode_InexistentFile: + return "Inexistent file"; - case ErrorCode_UriSyntax: - return "Badly formatted URI"; + case ErrorCode_BadFileFormat: + return "Bad file format"; + + case ErrorCode_CannotWriteFile: + return "Cannot write to file"; - case ErrorCode_BadParameterType: - return "Bad type for a parameter"; + case ErrorCode_Timeout: + return "Timeout"; - case ErrorCode_InexistentFile: - return "Inexistent file"; + case ErrorCode_UnknownResource: + return "Unknown resource"; - case ErrorCode_BadFileFormat: - return "Bad file format"; + case ErrorCode_BadSequenceOfCalls: + return "Bad sequence of calls"; + + case ErrorCode_IncompatibleDatabaseVersion: + return "Incompatible version of the database"; - case ErrorCode_CannotWriteFile: - return "Cannot write to file"; + case ErrorCode_FullStorage: + return "The file storage is full"; - case ErrorCode_Custom: - default: - return "???"; + case ErrorCode_InexistentItem: + return "Accessing an inexistent item"; + + case ErrorCode_Custom: + default: + return "???"; } } }
--- a/Core/OrthancException.h Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/OrthancException.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/Core/PngWriter.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/PngWriter.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/Core/PngWriter.h Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/PngWriter.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/RestApi/RestApi.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,288 @@ +/** + * 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 "RestApi.h" + +#include <stdlib.h> // To define "_exit()" under Windows +#include <glog/logging.h> + +namespace Orthanc +{ + bool RestApi::IsGetAccepted(const UriComponents& uri) + { + for (GetHandlers::const_iterator it = getHandlers_.begin(); + it != getHandlers_.end(); it++) + { + if (it->first->Match(uri)) + { + return true; + } + } + + return false; + } + + bool RestApi::IsPutAccepted(const UriComponents& uri) + { + for (PutHandlers::const_iterator it = putHandlers_.begin(); + it != putHandlers_.end(); it++) + { + if (it->first->Match(uri)) + { + return true; + } + } + + return false; + } + + bool RestApi::IsPostAccepted(const UriComponents& uri) + { + for (PostHandlers::const_iterator it = postHandlers_.begin(); + it != postHandlers_.end(); it++) + { + if (it->first->Match(uri)) + { + return true; + } + } + + return false; + } + + bool RestApi::IsDeleteAccepted(const UriComponents& uri) + { + for (DeleteHandlers::const_iterator it = deleteHandlers_.begin(); + it != deleteHandlers_.end(); it++) + { + if (it->first->Match(uri)) + { + return true; + } + } + + return false; + } + + static void AddMethod(std::string& target, + const std::string& method) + { + if (target.size() > 0) + target += "," + method; + else + target = method; + } + + std::string RestApi::GetAcceptedMethods(const UriComponents& uri) + { + std::string s; + + if (IsGetAccepted(uri)) + AddMethod(s, "GET"); + + if (IsPutAccepted(uri)) + AddMethod(s, "PUT"); + + if (IsPostAccepted(uri)) + AddMethod(s, "POST"); + + if (IsDeleteAccepted(uri)) + AddMethod(s, "DELETE"); + + return s; + } + + RestApi::~RestApi() + { + for (GetHandlers::iterator it = getHandlers_.begin(); + it != getHandlers_.end(); it++) + { + delete it->first; + } + + for (PutHandlers::iterator it = putHandlers_.begin(); + it != putHandlers_.end(); it++) + { + delete it->first; + } + + for (PostHandlers::iterator it = postHandlers_.begin(); + it != postHandlers_.end(); it++) + { + delete it->first; + } + + for (DeleteHandlers::iterator it = deleteHandlers_.begin(); + it != deleteHandlers_.end(); it++) + { + delete it->first; + } + } + + bool RestApi::IsServedUri(const UriComponents& uri) + { + return (IsGetAccepted(uri) || + IsPutAccepted(uri) || + IsPostAccepted(uri) || + IsDeleteAccepted(uri)); + } + + void RestApi::Handle(HttpOutput& output, + const std::string& method, + const UriComponents& uri, + const Arguments& headers, + const Arguments& getArguments, + const std::string& postData) + { + bool ok = false; + RestApiOutput restOutput(output); + RestApiPath::Components components; + UriComponents trailing; + + if (method == "GET") + { + for (GetHandlers::const_iterator it = getHandlers_.begin(); + it != getHandlers_.end(); it++) + { + if (it->first->Match(components, trailing, uri)) + { + LOG(INFO) << "REST GET call on: " << Toolbox::FlattenUri(uri); + ok = true; + GetCall call; + call.output_ = &restOutput; + call.context_ = this; + call.httpHeaders_ = &headers; + call.uriComponents_ = &components; + call.trailing_ = &trailing; + call.fullUri_ = &uri; + + call.getArguments_ = &getArguments; + it->second(call); + } + } + } + else if (method == "PUT") + { + for (PutHandlers::const_iterator it = putHandlers_.begin(); + it != putHandlers_.end(); it++) + { + if (it->first->Match(components, trailing, uri)) + { + LOG(INFO) << "REST PUT call on: " << Toolbox::FlattenUri(uri); + ok = true; + PutCall call; + call.output_ = &restOutput; + call.context_ = this; + call.httpHeaders_ = &headers; + call.uriComponents_ = &components; + call.trailing_ = &trailing; + call.fullUri_ = &uri; + + call.data_ = &postData; + it->second(call); + } + } + } + else if (method == "POST") + { + for (PostHandlers::const_iterator it = postHandlers_.begin(); + it != postHandlers_.end(); it++) + { + if (it->first->Match(components, trailing, uri)) + { + LOG(INFO) << "REST POST call on: " << Toolbox::FlattenUri(uri); + ok = true; + PostCall call; + call.output_ = &restOutput; + call.context_ = this; + call.httpHeaders_ = &headers; + call.uriComponents_ = &components; + call.trailing_ = &trailing; + call.fullUri_ = &uri; + + call.data_ = &postData; + it->second(call); + } + } + } + else if (method == "DELETE") + { + for (DeleteHandlers::const_iterator it = deleteHandlers_.begin(); + it != deleteHandlers_.end(); it++) + { + if (it->first->Match(components, trailing, uri)) + { + LOG(INFO) << "REST DELETE call on: " << Toolbox::FlattenUri(uri); + ok = true; + DeleteCall call; + call.output_ = &restOutput; + call.context_ = this; + call.httpHeaders_ = &headers; + call.uriComponents_ = &components; + call.trailing_ = &trailing; + call.fullUri_ = &uri; + it->second(call); + } + } + } + + if (!ok) + { + LOG(INFO) << "REST method " << method << " not allowed on: " << Toolbox::FlattenUri(uri); + output.SendMethodNotAllowedError(GetAcceptedMethods(uri)); + } + } + + void RestApi::Register(const std::string& path, + GetHandler handler) + { + getHandlers_.push_back(std::make_pair(new RestApiPath(path), handler)); + } + + void RestApi::Register(const std::string& path, + PutHandler handler) + { + putHandlers_.push_back(std::make_pair(new RestApiPath(path), handler)); + } + + void RestApi::Register(const std::string& path, + PostHandler handler) + { + postHandlers_.push_back(std::make_pair(new RestApiPath(path), handler)); + } + + void RestApi::Register(const std::string& path, + DeleteHandler handler) + { + deleteHandlers_.push_back(std::make_pair(new RestApiPath(path), handler)); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/RestApi/RestApi.h Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,200 @@ +/** + * 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 "../HttpServer/HttpHandler.h" +#include "RestApiPath.h" +#include "RestApiOutput.h" + +#include <list> + +namespace Orthanc +{ + class RestApi : public HttpHandler + { + private: + class SharedCall + { + friend class RestApi; + + private: + RestApiOutput* output_; + RestApi* context_; + const HttpHandler::Arguments* httpHeaders_; + const RestApiPath::Components* uriComponents_; + const UriComponents* trailing_; + const UriComponents* fullUri_; + + public: + RestApiOutput& GetOutput() + { + return *output_; + } + + RestApi& GetContext() + { + return *context_; + } + + const UriComponents& GetFullUri() const + { + return *fullUri_; + } + + const UriComponents& GetTrailingUri() const + { + return *trailing_; + } + + std::string GetUriComponent(const std::string& name, + const std::string& defaultValue) const + { + return HttpHandler::GetArgument(*uriComponents_, name, defaultValue); + } + + std::string GetHttpHeader(const std::string& name, + const std::string& defaultValue) const + { + return HttpHandler::GetArgument(*httpHeaders_, name, defaultValue); + } + }; + + + public: + class GetCall : public SharedCall + { + friend class RestApi; + + private: + const HttpHandler::Arguments* getArguments_; + + public: + std::string GetArgument(const std::string& name, + const std::string& defaultValue) const + { + return HttpHandler::GetArgument(*getArguments_, name, defaultValue); + } + + bool HasArgument(const std::string& name) const + { + return getArguments_->find(name) != getArguments_->end(); + } + }; + + class PutCall : public SharedCall + { + friend class RestApi; + + private: + const std::string* data_; + + public: + const std::string& GetPutBody() + { + return *data_; + } + }; + + class PostCall : public SharedCall + { + friend class RestApi; + + private: + const std::string* data_; + + public: + const std::string& GetPostBody() + { + return *data_; + } + }; + + class DeleteCall : public SharedCall + { + }; + + typedef void (*GetHandler) (GetCall& call); + + typedef void (*DeleteHandler) (DeleteCall& call); + + typedef void (*PutHandler) (PutCall& call); + + typedef void (*PostHandler) (PostCall& call); + + private: + typedef std::list< std::pair<RestApiPath*, GetHandler> > GetHandlers; + typedef std::list< std::pair<RestApiPath*, PutHandler> > PutHandlers; + typedef std::list< std::pair<RestApiPath*, PostHandler> > PostHandlers; + typedef std::list< std::pair<RestApiPath*, DeleteHandler> > DeleteHandlers; + + GetHandlers getHandlers_; + PutHandlers putHandlers_; + PostHandlers postHandlers_; + DeleteHandlers deleteHandlers_; + + bool IsGetAccepted(const UriComponents& uri); + bool IsPutAccepted(const UriComponents& uri); + bool IsPostAccepted(const UriComponents& uri); + bool IsDeleteAccepted(const UriComponents& uri); + + std::string GetAcceptedMethods(const UriComponents& uri); + + public: + RestApi() + { + } + + ~RestApi(); + + virtual bool IsServedUri(const UriComponents& uri); + + virtual void Handle(HttpOutput& output, + const std::string& method, + const UriComponents& uri, + const Arguments& headers, + const Arguments& getArguments, + const std::string& postData); + + void Register(const std::string& path, + GetHandler handler); + + void Register(const std::string& path, + PutHandler handler); + + void Register(const std::string& path, + PostHandler handler); + + void Register(const std::string& path, + DeleteHandler handler); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/RestApi/RestApiOutput.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,103 @@ +/** + * 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 "RestApiOutput.h" + +#include "../OrthancException.h" + +namespace Orthanc +{ + RestApiOutput::RestApiOutput(HttpOutput& output) : + output_(output) + { + alreadySent_ = false; + } + + RestApiOutput::~RestApiOutput() + { + if (!alreadySent_) + { + output_.SendHeader(Orthanc_HttpStatus_400_BadRequest); + } + } + + void RestApiOutput::CheckStatus() + { + if (alreadySent_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + void RestApiOutput::AnswerFile(HttpFileSender& sender) + { + CheckStatus(); + sender.Send(output_); + alreadySent_ = true; + } + + void RestApiOutput::AnswerJson(const Json::Value& value) + { + CheckStatus(); + Json::StyledWriter writer; + std::string s = writer.write(value); + output_.AnswerBufferWithContentType(s, "application/json"); + alreadySent_ = true; + } + + void RestApiOutput::AnswerBuffer(const std::string& buffer, + const std::string& contentType) + { + CheckStatus(); + output_.AnswerBufferWithContentType(buffer, contentType); + alreadySent_ = true; + } + + void RestApiOutput::Redirect(const std::string& path) + { + CheckStatus(); + output_.Redirect(path); + alreadySent_ = true; + } + + void RestApiOutput::SignalError(Orthanc_HttpStatus status) + { + if (status != Orthanc_HttpStatus_415_UnsupportedMediaType) + { + throw OrthancException("This HTTP status is not allowed in a REST API"); + } + + CheckStatus(); + output_.SendHeader(status); + alreadySent_ = true; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/RestApi/RestApiOutput.h Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,76 @@ +/** + * 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 "../HttpServer/HttpOutput.h" +#include "../HttpServer/HttpFileSender.h" + +#include <json/json.h> + +namespace Orthanc +{ + class RestApiOutput + { + private: + HttpOutput& output_; + bool alreadySent_; + + void CheckStatus(); + + public: + RestApiOutput(HttpOutput& output); + + ~RestApiOutput(); + + HttpOutput& GetLowLevelOutput() + { + return output_; + } + + void MarkLowLevelOutputDone() + { + alreadySent_ = true; + } + + void AnswerFile(HttpFileSender& sender); + + void AnswerJson(const Json::Value& value); + + void AnswerBuffer(const std::string& buffer, + const std::string& contentType); + + void SignalError(Orthanc_HttpStatus status); + + void Redirect(const std::string& path); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/RestApi/RestApiPath.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,137 @@ +/** + * 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 "RestApiPath.h" + +#include <cassert> + +namespace Orthanc +{ + RestApiPath::RestApiPath(const std::string& uri) + { + Toolbox::SplitUriComponents(uri_, uri); + + if (uri_.size() == 0) + { + hasTrailing_ = false; + return; + } + + if (uri_.back() == "*") + { + hasTrailing_ = true; + uri_.pop_back(); + } + else + { + hasTrailing_ = false; + } + + components_.resize(uri_.size()); + for (size_t i = 0; i < uri_.size(); i++) + { + size_t s = uri_[i].size(); + assert(s > 0); + + if (uri_[i][0] == '{' && + uri_[i][s - 1] == '}') + { + components_[i] = uri_[i].substr(1, s - 2); + uri_[i] = ""; + } + else + { + components_[i] = ""; + } + } + } + + bool RestApiPath::Match(Components& components, + UriComponents& trailing, + const std::string& uriRaw) const + { + UriComponents uri; + Toolbox::SplitUriComponents(uri, uriRaw); + return Match(components, trailing, uri); + } + + bool RestApiPath::Match(Components& components, + UriComponents& trailing, + const UriComponents& uri) const + { + if (uri.size() < uri_.size()) + { + return false; + } + + if (!hasTrailing_ && uri.size() > uri_.size()) + { + return false; + } + + components.clear(); + trailing.clear(); + + assert(uri_.size() <= uri.size()); + for (size_t i = 0; i < uri_.size(); i++) + { + if (components_[i].size() == 0) + { + // This URI component is not a free parameter + if (uri_[i] != uri[i]) + { + return false; + } + } + else + { + // This URI component is a free parameter + components[components_[i]] = uri[i]; + } + } + + if (hasTrailing_) + { + trailing.assign(uri.begin() + uri_.size(), uri.end()); + } + + return true; + } + + + bool RestApiPath::Match(const UriComponents& uri) const + { + Components components; + UriComponents trailing; + return Match(components, trailing, uri); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/RestApi/RestApiPath.h Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,63 @@ +/** + * 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 "../Toolbox.h" +#include <map> + +namespace Orthanc +{ + class RestApiPath + { + private: + UriComponents uri_; + bool hasTrailing_; + std::vector<std::string> components_; + + public: + typedef std::map<std::string, std::string> Components; + + RestApiPath(const std::string& uri); + + // This version is slower + bool Match(Components& components, + UriComponents& trailing, + const std::string& uriRaw) const; + + bool Match(Components& components, + UriComponents& trailing, + const UriComponents& uri) const; + + bool Match(const UriComponents& uri) const; + }; +}
--- a/Core/SQLite/Connection.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/SQLite/Connection.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -41,6 +41,7 @@ #include <sqlite3.h> #include <string.h> +#include <glog/logging.h> namespace Orthanc @@ -88,6 +89,8 @@ // http://www.sqlite.org/pragma.html Execute("PRAGMA FOREIGN_KEYS=ON;"); + Execute("PRAGMA RECURSIVE_TRIGGERS=ON;"); + // Performance tuning Execute("PRAGMA SYNCHRONOUS=NORMAL;"); Execute("PRAGMA JOURNAL_MODE=WAL;"); @@ -149,6 +152,7 @@ bool Connection::Execute(const char* sql) { + VLOG(1) << "SQLite::Connection::Execute " << sql; CheckIsOpen(); int error = sqlite3_exec(db_, sql, NULL, NULL, NULL); @@ -368,5 +372,16 @@ return func; } + + void Connection::FlushToDisk() + { + VLOG(1) << "SQLite::Connection::FlushToDisk"; + int err = sqlite3_wal_checkpoint(db_, NULL); + + if (err != SQLITE_OK) + { + throw OrthancException("SQLite: Unable to flush the database"); + } + } } }
--- a/Core/SQLite/Connection.h Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/SQLite/Connection.h Fri Dec 14 11:24:24 2012 +0100 @@ -111,6 +111,8 @@ return Execute(sql.c_str()); } + void FlushToDisk(); + IScalarFunction* Register(IScalarFunction* func); // Takes the ownership of the function // Info querying ------------------------------------------------------------- @@ -169,7 +171,7 @@ bool BeginTransaction(); void RollbackTransaction(); - bool CommitTransaction(); + bool CommitTransaction(); }; } }
--- a/Core/SQLite/FunctionContext.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/SQLite/FunctionContext.cpp Fri Dec 14 11:24:24 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 Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/SQLite/FunctionContext.h Fri Dec 14 11:24:24 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/SQLite/Statement.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/SQLite/Statement.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -42,6 +42,7 @@ #include <boost/lexical_cast.hpp> #include <sqlite3.h> #include <string.h> +#include <glog/logging.h> namespace Orthanc { @@ -106,11 +107,13 @@ bool Statement::Run() { + VLOG(1) << "SQLite::Statement::Run " << sqlite3_sql(GetStatement()); return CheckError(sqlite3_step(GetStatement())) == SQLITE_DONE; } bool Statement::Step() { + VLOG(1) << "SQLite::Statement::Step " << sqlite3_sql(GetStatement()); return CheckError(sqlite3_step(GetStatement())) == SQLITE_ROW; } @@ -121,6 +124,7 @@ // spurious error callback. if (clear_bound_vars) sqlite3_clear_bindings(GetStatement()); + //VLOG(1) << "SQLite::Statement::Reset"; sqlite3_reset(GetStatement()); }
--- a/Core/SQLite/Statement.h Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/SQLite/Statement.h Fri Dec 14 11:24:24 2012 +0100 @@ -44,6 +44,10 @@ #include <stdint.h> #include <boost/noncopyable.hpp> +#if ORTHANC_BUILD_UNIT_TESTS == 1 +#include <gtest/gtest_prod.h> +#endif + struct sqlite3_stmt; @@ -68,6 +72,11 @@ { friend class Connection; +#if ORTHANC_BUILD_UNIT_TESTS == 1 + FRIEND_TEST(SQLStatementTest, Run); + FRIEND_TEST(SQLStatementTest, Reset); +#endif + private: StatementReference reference_; @@ -80,6 +89,10 @@ return reference_.GetWrappedObject(); } + // Resets the statement to its initial condition. This includes any current + // result row, and also the bound variables if the |clear_bound_vars| is true. + void Reset(bool clear_bound_vars = true); + public: Statement(Connection& database, const std::string& sql); @@ -95,14 +108,15 @@ const StatementId& id, const char* sql); + ~Statement() + { + Reset(); + } + bool Run(); bool Step(); - // Resets the statement to its initial condition. This includes any current - // result row, and also the bound variables if the |clear_bound_vars| is true. - void Reset(bool clear_bound_vars = true); - // Diagnostics -------------------------------------------------------------- std::string GetOriginalSQLStatement();
--- a/Core/Toolbox.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/Toolbox.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -25,6 +37,7 @@ #include <string.h> #include <boost/filesystem.hpp> #include <boost/filesystem/fstream.hpp> +#include <boost/date_time/posix_time/posix_time.hpp> #include <algorithm> #include <ctype.h> @@ -51,6 +64,7 @@ #include "../Resources/md5/md5.h" #include "../Resources/base64/base64.h" +#include "../Resources/sha1/sha1.h" #if BOOST_HAS_LOCALE == 0 @@ -274,6 +288,15 @@ { components.push_back(std::string(&uri[start], end - start)); } + + for (size_t i = 0; i < components.size(); i++) + { + if (components[i].size() == 0) + { + // Empty component, as in: "/coucou//e" + throw OrthancException(ErrorCode_UriSyntax); + } + } } @@ -506,4 +529,67 @@ return result; } + + void Toolbox::ComputeSHA1(std::string& result, + const std::string& data) + { + SHA1 sha1; + if (data.size() > 0) + { + sha1.Input(&data[0], data.size()); + } + + unsigned digest[5]; + + // Sanity check for the memory layout: A SHA-1 digest is 160 bits wide + assert(sizeof(unsigned) == 4 && sizeof(digest) == (160 / 8)); + + if (sha1.Result(digest)) + { + result.resize(8 * 5 + 4); + sprintf(&result[0], "%08x-%08x-%08x-%08x-%08x", + digest[0], + digest[1], + digest[2], + digest[3], + digest[4]); + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + + std::string Toolbox::GetNowIsoString() + { + boost::posix_time::ptime now = boost::posix_time::second_clock::local_time(); + return boost::posix_time::to_iso_string(now); + } + + std::string Toolbox::StripSpaces(const std::string& source) + { + size_t first = 0; + + while (first < source.length() && + isspace(source[first])) + { + first++; + } + + if (first == source.length()) + { + // String containing only spaces + return ""; + } + + size_t last = source.length(); + while (last > first && + isspace(source[last - 1])) + { + last--; + } + + assert(first <= last); + return source.substr(first, last - first); + } }
--- a/Core/Toolbox.h Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/Toolbox.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -28,6 +40,10 @@ { typedef std::vector<std::string> UriComponents; + class NullType + { + }; + namespace Toolbox { void ServerBarrier(); @@ -61,6 +77,9 @@ void ComputeMD5(std::string& result, const std::string& data); + void ComputeSHA1(std::string& result, + const std::string& data); + std::string EncodeBase64(const std::string& data); std::string GetPathToExecutable(); @@ -71,5 +90,9 @@ const char* fromEncoding); std::string ConvertToAscii(const std::string& source); + + std::string StripSpaces(const std::string& source); + + std::string GetNowIsoString(); } }
--- a/Core/Uuid.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/Uuid.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/Core/Uuid.h Thu Oct 04 15:09:56 2012 +0200 +++ b/Core/Uuid.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/INSTALL Thu Oct 04 15:09:56 2012 +0200 +++ b/INSTALL Fri Dec 14 11:24:24 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 Thu Oct 04 15:09:56 2012 +0200 +++ b/NEWS Fri Dec 14 11:24:24 2012 +0100 @@ -1,6 +1,54 @@ 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) +========================== + +* Download archives of patients, studies and series as ZIP files +* Orthanc now checks the version of its database schema before starting + + +Version 0.3.0 (2012/11/30) +========================== + +Major changes +------------- + +* Transparent compression of the DICOM instances on the disk +* The patient/study/series/instances are now indexed by SHA-1 digests + of their DICOM Instance IDs (and not by UUIDs anymore): The same + DICOM objects are thus always identified by the same Orthanc IDs +* Log of exported instances through DICOM C-Store SCU ("/exported" URI) +* Full refactoring of the DB schema and of the REST API +* Introduction of generic classes for REST APIs (in Core/RestApi) + +Minor changes +------------- + +* "/statistics" URI +* "last" flag to retrieve the last change from the "/changes" URI +* Generate a sample configuration file from command line +* "CompletedSeries" event in the changes API +* Thread to continuously flush DB to disk (SQLite checkpoints for + improved robustness) + + +Version 0.2.3 (2012/10/26) +========================== + +* Use HTTP Content-Disposition to set a filename when downloading JSON/DCM +* URI "/system" for general information about Orthanc +* Versioning info and help on the command line +* Improved logging +* Possibility of dynamic linking against jsoncpp, sqlite, boost and dmctk + for Debian packaging +* Fix some bugs +* Switch to default 8042 port for HTTP Version 0.2.2 (2012/10/04) @@ -10,7 +58,6 @@ * Fixes to Debian packaging - Version 0.2.1 (2012/09/28) ========================== @@ -19,7 +66,6 @@ * Ready for Debian packaging - Version 0.2.0 (2012/09/16) ========================== @@ -41,7 +87,6 @@ * Standalone build for cross-compilation - Version 0.1.1 (2012/07/20) ========================== @@ -50,7 +95,6 @@ * Add path to storage in Configuration.json - Version 0.1.0 (2012/07/19) ==========================
--- a/OrthancCppClient/HttpClient.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancCppClient/HttpClient.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -197,4 +197,12 @@ return false; } } + + + void HttpClient::SetPassword(const char* username, + const char* password) + { + std::string s = std::string(username) + ":" + std::string(password); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_USERPWD, s.c_str())); + } }
--- a/OrthancCppClient/HttpClient.h Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancCppClient/HttpClient.h Fri Dec 14 11:24:24 2012 +0100 @@ -109,5 +109,7 @@ return HttpException::GetDescription(lastStatus_); } + void SetPassword(const char* username, + const char* password); }; }
--- a/OrthancExplorer/explorer.css Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancExplorer/explorer.css Fri Dec 14 11:24:24 2012 +0100 @@ -32,3 +32,12 @@ width: 0%; background-color: green; } + +.ui-title a { + text-decoration: none; + color: white !important; +} + +.switch-container .ui-slider-switch { + width: 100%; +} \ No newline at end of file
--- a/OrthancExplorer/explorer.html Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancExplorer/explorer.html Fri Dec 14 11:24:24 2012 +0100 @@ -20,7 +20,7 @@ <script src="libs/date.js"></script> <script src="libs/jquery.mobile.simpledialog2.js"></script> <script src="libs/slimbox2.js"></script> - <script src="libs/jquery.blockUI.js"></script> + <script src="libs/jquery.blockui.js"></script> <!-- https://github.com/blueimp/jQuery-File-Upload/wiki/Basic-plugin --> <script src="libs/jquery-file-upload/js/vendor/jquery.ui.widget.js"></script> @@ -34,7 +34,7 @@ <body> <div data-role="page" id="find-patients" > <div data-role="header" > - <h1>Find a patient</h1> + <h1><span class="orthanc-name"></span>Find a patient</h1> <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a> </div> <div data-role="content"> @@ -45,7 +45,7 @@ <div data-role="page" id="upload" > <div data-role="header" > - <h1>Upload DICOM files</h1> + <h1><span class="orthanc-name"></span>Upload DICOM files</h1> <a href="#find-patients" data-icon="search" class="ui-btn-left" data-direction="reverse">Find patient</a> </div> <div data-role="content"> @@ -71,7 +71,7 @@ <div data-role="page" id="patient" > <div data-role="header" > - <h1>List of the studies of one patient</h1> + <h1><span class="orthanc-name"></span>Patient</h1> <a href="#find-patients" data-icon="search" class="ui-btn-left" data-direction="reverse">Find patient</a> <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a> </div> @@ -82,8 +82,15 @@ <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> </div> </div> @@ -99,7 +106,11 @@ <div data-role="page" id="study"> <div data-role="header"> - <h1>List of the series of one study</h1> + <h1> + <span class="orthanc-name"></span> + <a href="#" class="patient-link">Patient</a> » + Study + </h1> <a href="#find-patients" data-icon="search" class="ui-btn-left" data-direction="reverse">Find patient</a> <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a> </div> @@ -111,6 +122,7 @@ </ul> <p> <a href="#" data-role="button" data-icon="delete" id="study-delete">Delete this study</a> + <a href="#" data-role="button" data-icon="gear" id="study-archive">Download ZIP</a> </p> </div> </div> @@ -126,7 +138,13 @@ <div data-role="page" id="series"> <div data-role="header"> - <h1>List of the instances of one series</h1> + <h1> + <span class="orthanc-name"></span> + <a href="#" class="patient-link">Patient</a> » + <a href="#" class="study-link">Study</a> » + Series + </h1> + <a href="#find-patients" data-icon="search" class="ui-btn-left" data-direction="reverse">Find patient</a> <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a> </div> @@ -138,8 +156,9 @@ </ul> <p> <a href="#" data-role="button" data-icon="delete" id="series-delete">Delete this series</a> - <a href="#" data-role="button" data-icon="arrow-d" id="series-preview">Preview this series</a> - <a href="#" data-role="button" data-icon="arrow-d" id="series-store">Store in another DICOM modality</a> + <a href="#" data-role="button" data-icon="search" id="series-preview">Preview this series</a> + <a href="#" data-role="button" data-icon="forward" id="series-store">Store in another DICOM modality</a> + <a href="#" data-role="button" data-icon="gear" id="series-archive">Download ZIP</a> </p> </div> </div> @@ -155,7 +174,13 @@ <div data-role="page" id="instance"> <div data-role="header"> - <h1>One DICOM instance</h1> + <h1> + <span class="orthanc-name"></span> + <a href="#" class="patient-link">Patient</a> » + <a href="#" class="study-link">Study</a> » + <a href="#" class="series-link">Series</a> » + Instance + </h1> <a href="#find-patients" data-icon="search" class="ui-btn-left" data-direction="reverse">Find patient</a> <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a> </div> @@ -169,8 +194,8 @@ <a href="#" data-role="button" data-icon="delete" id="instance-delete">Delete this instance</a> <a href="#" data-role="button" data-icon="arrow-d" id="instance-download-dicom">Download the DICOM file</a> <a href="#" data-role="button" data-icon="arrow-d" id="instance-download-json">Download the JSON file</a> - <a href="#" data-role="button" data-icon="arrow-d" id="instance-preview">Preview the instance</a> - <a href="#" data-role="button" data-icon="arrow-d" id="instance-store">Store in another DICOM modality</a> + <a href="#" data-role="button" data-icon="search" id="instance-preview">Preview the instance</a> + <a href="#" data-role="button" data-icon="forward" id="instance-store">Store in another DICOM modality</a> </p> </div> </div>
--- a/OrthancExplorer/explorer.js Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancExplorer/explorer.js Fri Dec 14 11:24:24 2012 +0100 @@ -319,6 +319,19 @@ } +$('[data-role="page"]').live('pagebeforeshow', function() { + $.ajax({ + url: '../system', + dataType: 'json', + async: false, + success: function(s) { + if (s.Name != "") { + $('.orthanc-name').html('<a class="ui-link" href="explorer.html">' + s.Name + '</a> » '); + } + } + }); +}); + $('#find-patients').live('pagebeforeshow', function() { @@ -365,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'); + } + }); }); }); } @@ -378,6 +403,7 @@ GetMultipleResources('series', study.Series, function(series) { SortOnDicomTag(series, 'SeriesDate', false, true); + $('#study .patient-link').attr('href', '#patient?uuid=' + patient.ID); $('#study-info li').remove(); $('#study-info') .append('<li data-role="list-divider">Patient</li>') @@ -412,6 +438,9 @@ GetMultipleResources('instances', series.Instances, function(instances) { Sort(instances, function(x) { return x.IndexInSeries; }, true, false); + $('#series .patient-link').attr('href', '#patient?uuid=' + patient.ID); + $('#series .study-link').attr('href', '#study?uuid=' + study.ID); + $('#series-info li').remove(); $('#series-info') .append('<li data-role="list-divider">Patient</li>') @@ -494,6 +523,10 @@ GetSingleResource('series', instance.ParentSeries, function(series) { GetSingleResource('studies', series.ParentStudy, function(study) { GetSingleResource('patients', study.ParentPatient, function(patient) { + + $('#instance .patient-link').attr('href', '#patient?uuid=' + patient.ID); + $('#instance .study-link').attr('href', '#study?uuid=' + study.ID); + $('#instance .series-link').attr('href', '#series?uuid=' + series.ID); $('#instance-info li').remove(); $('#instance-info') @@ -536,7 +569,7 @@ if (ancestor == null) $.mobile.changePage('#find-patients'); else - $.mobile.changePage('#' + ancestor.Type + '?uuid=' + ancestor.ID); + $.mobile.changePage('#' + ancestor.Type.toLowerCase() + '?uuid=' + ancestor.ID); } }); } @@ -605,6 +638,7 @@ }); + $('#instance-preview').live('click', function(e) { if ($.mobile.pageData) { GetSingleResource('instances', $.mobile.pageData.uuid + '/frames', function(frames) { @@ -747,3 +781,30 @@ else $('.tag-name').hide(); }); + + +$('#patient-archive').live('click', function(e) { + e.preventDefault(); //stop the browser from following + window.location.href = '../patients/' + $.mobile.pageData.uuid + '/archive'; +}); + +$('#study-archive').live('click', function(e) { + e.preventDefault(); //stop the browser from following + window.location.href = '../studies/' + $.mobile.pageData.uuid + '/archive'; +}); + +$('#series-archive').live('click', function(e) { + e.preventDefault(); //stop the browser from following + 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/OrthancExplorer/libs/jquery.blockUI.js Thu Oct 04 15:09:56 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,499 +0,0 @@ -/*! - * jQuery blockUI plugin - * Version 2.39 (23-MAY-2011) - * @requires jQuery v1.2.3 or later - * - * Examples at: http://malsup.com/jquery/block/ - * Copyright (c) 2007-2010 M. Alsup - * Dual licensed under the MIT and GPL licenses: - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html - * - * Thanks to Amir-Hossein Sobhi for some excellent contributions! - */ - -;(function($) { - -if (/1\.(0|1|2)\.(0|1|2)/.test($.fn.jquery) || /^1.1/.test($.fn.jquery)) { - alert('blockUI requires jQuery v1.2.3 or later! You are using v' + $.fn.jquery); - return; -} - -$.fn._fadeIn = $.fn.fadeIn; - -var noOp = function() {}; - -// this bit is to ensure we don't call setExpression when we shouldn't (with extra muscle to handle -// retarded userAgent strings on Vista) -var mode = document.documentMode || 0; -var setExpr = $.browser.msie && (($.browser.version < 8 && !mode) || mode < 8); -var ie6 = $.browser.msie && /MSIE 6.0/.test(navigator.userAgent) && !mode; - -// global $ methods for blocking/unblocking the entire page -$.blockUI = function(opts) { install(window, opts); }; -$.unblockUI = function(opts) { remove(window, opts); }; - -// convenience method for quick growl-like notifications (http://www.google.com/search?q=growl) -$.growlUI = function(title, message, timeout, onClose) { - var $m = $('<div class="growlUI"></div>'); - if (title) $m.append('<h1>'+title+'</h1>'); - if (message) $m.append('<h2>'+message+'</h2>'); - if (timeout == undefined) timeout = 3000; - $.blockUI({ - message: $m, fadeIn: 700, fadeOut: 1000, centerY: false, - timeout: timeout, showOverlay: false, - onUnblock: onClose, - css: $.blockUI.defaults.growlCSS - }); -}; - -// plugin method for blocking element content -$.fn.block = function(opts) { - return this.unblock({ fadeOut: 0 }).each(function() { - if ($.css(this,'position') == 'static') - this.style.position = 'relative'; - if ($.browser.msie) - this.style.zoom = 1; // force 'hasLayout' - install(this, opts); - }); -}; - -// plugin method for unblocking element content -$.fn.unblock = function(opts) { - return this.each(function() { - remove(this, opts); - }); -}; - -$.blockUI.version = 2.39; // 2nd generation blocking at no extra cost! - -// override these in your code to change the default behavior and style -$.blockUI.defaults = { - // message displayed when blocking (use null for no message) - message: '<h1>Please wait...</h1>', - - title: null, // title string; only used when theme == true - draggable: true, // only used when theme == true (requires jquery-ui.js to be loaded) - - theme: false, // set to true to use with jQuery UI themes - - // styles for the message when blocking; if you wish to disable - // these and use an external stylesheet then do this in your code: - // $.blockUI.defaults.css = {}; - css: { - padding: 0, - margin: 0, - width: '30%', - top: '40%', - left: '35%', - textAlign: 'center', - color: '#000', - border: '3px solid #aaa', - backgroundColor:'#fff', - cursor: 'wait' - }, - - // minimal style set used when themes are used - themedCSS: { - width: '30%', - top: '40%', - left: '35%' - }, - - // styles for the overlay - overlayCSS: { - backgroundColor: '#000', - opacity: 0.6, - cursor: 'wait' - }, - - // styles applied when using $.growlUI - growlCSS: { - width: '350px', - top: '10px', - left: '', - right: '10px', - border: 'none', - padding: '5px', - opacity: 0.6, - cursor: 'default', - color: '#fff', - backgroundColor: '#000', - '-webkit-border-radius': '10px', - '-moz-border-radius': '10px', - 'border-radius': '10px' - }, - - // IE issues: 'about:blank' fails on HTTPS and javascript:false is s-l-o-w - // (hat tip to Jorge H. N. de Vasconcelos) - iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank', - - // force usage of iframe in non-IE browsers (handy for blocking applets) - forceIframe: false, - - // z-index for the blocking overlay - baseZ: 1000, - - // set these to true to have the message automatically centered - centerX: true, // <-- only effects element blocking (page block controlled via css above) - centerY: true, - - // allow body element to be stetched in ie6; this makes blocking look better - // on "short" pages. disable if you wish to prevent changes to the body height - allowBodyStretch: true, - - // enable if you want key and mouse events to be disabled for content that is blocked - bindEvents: true, - - // be default blockUI will supress tab navigation from leaving blocking content - // (if bindEvents is true) - constrainTabKey: true, - - // fadeIn time in millis; set to 0 to disable fadeIn on block - fadeIn: 200, - - // fadeOut time in millis; set to 0 to disable fadeOut on unblock - fadeOut: 400, - - // time in millis to wait before auto-unblocking; set to 0 to disable auto-unblock - timeout: 0, - - // disable if you don't want to show the overlay - showOverlay: true, - - // if true, focus will be placed in the first available input field when - // page blocking - focusInput: true, - - // suppresses the use of overlay styles on FF/Linux (due to performance issues with opacity) - applyPlatformOpacityRules: true, - - // callback method invoked when fadeIn has completed and blocking message is visible - onBlock: null, - - // callback method invoked when unblocking has completed; the callback is - // passed the element that has been unblocked (which is the window object for page - // blocks) and the options that were passed to the unblock call: - // onUnblock(element, options) - onUnblock: null, - - // don't ask; if you really must know: http://groups.google.com/group/jquery-en/browse_thread/thread/36640a8730503595/2f6a79a77a78e493#2f6a79a77a78e493 - quirksmodeOffsetHack: 4, - - // class name of the message block - blockMsgClass: 'blockMsg' -}; - -// private data and functions follow... - -var pageBlock = null; -var pageBlockEls = []; - -function install(el, opts) { - var full = (el == window); - var msg = opts && opts.message !== undefined ? opts.message : undefined; - opts = $.extend({}, $.blockUI.defaults, opts || {}); - opts.overlayCSS = $.extend({}, $.blockUI.defaults.overlayCSS, opts.overlayCSS || {}); - var css = $.extend({}, $.blockUI.defaults.css, opts.css || {}); - var themedCSS = $.extend({}, $.blockUI.defaults.themedCSS, opts.themedCSS || {}); - msg = msg === undefined ? opts.message : msg; - - // remove the current block (if there is one) - if (full && pageBlock) - remove(window, {fadeOut:0}); - - // if an existing element is being used as the blocking content then we capture - // its current place in the DOM (and current display style) so we can restore - // it when we unblock - if (msg && typeof msg != 'string' && (msg.parentNode || msg.jquery)) { - var node = msg.jquery ? msg[0] : msg; - var data = {}; - $(el).data('blockUI.history', data); - data.el = node; - data.parent = node.parentNode; - data.display = node.style.display; - data.position = node.style.position; - if (data.parent) - data.parent.removeChild(node); - } - - $(el).data('blockUI.onUnblock', opts.onUnblock); - var z = opts.baseZ; - - // blockUI uses 3 layers for blocking, for simplicity they are all used on every platform; - // layer1 is the iframe layer which is used to supress bleed through of underlying content - // layer2 is the overlay layer which has opacity and a wait cursor (by default) - // layer3 is the message content that is displayed while blocking - - var lyr1 = ($.browser.msie || opts.forceIframe) - ? $('<iframe class="blockUI" style="z-index:'+ (z++) +';display:none;border:none;margin:0;padding:0;position:absolute;width:100%;height:100%;top:0;left:0" src="'+opts.iframeSrc+'"></iframe>') - : $('<div class="blockUI" style="display:none"></div>'); - - var lyr2 = opts.theme - ? $('<div class="blockUI blockOverlay ui-widget-overlay" style="z-index:'+ (z++) +';display:none"></div>') - : $('<div class="blockUI blockOverlay" style="z-index:'+ (z++) +';display:none;border:none;margin:0;padding:0;width:100%;height:100%;top:0;left:0"></div>'); - - var lyr3, s; - if (opts.theme && full) { - s = '<div class="blockUI ' + opts.blockMsgClass + ' blockPage ui-dialog ui-widget ui-corner-all" style="z-index:'+(z+10)+';display:none;position:fixed">' + - '<div class="ui-widget-header ui-dialog-titlebar ui-corner-all blockTitle">'+(opts.title || ' ')+'</div>' + - '<div class="ui-widget-content ui-dialog-content"></div>' + - '</div>'; - } - else if (opts.theme) { - s = '<div class="blockUI ' + opts.blockMsgClass + ' blockElement ui-dialog ui-widget ui-corner-all" style="z-index:'+(z+10)+';display:none;position:absolute">' + - '<div class="ui-widget-header ui-dialog-titlebar ui-corner-all blockTitle">'+(opts.title || ' ')+'</div>' + - '<div class="ui-widget-content ui-dialog-content"></div>' + - '</div>'; - } - else if (full) { - s = '<div class="blockUI ' + opts.blockMsgClass + ' blockPage" style="z-index:'+(z+10)+';display:none;position:fixed"></div>'; - } - else { - s = '<div class="blockUI ' + opts.blockMsgClass + ' blockElement" style="z-index:'+(z+10)+';display:none;position:absolute"></div>'; - } - lyr3 = $(s); - - // if we have a message, style it - if (msg) { - if (opts.theme) { - lyr3.css(themedCSS); - lyr3.addClass('ui-widget-content'); - } - else - lyr3.css(css); - } - - // style the overlay - if (!opts.theme && (!opts.applyPlatformOpacityRules || !($.browser.mozilla && /Linux/.test(navigator.platform)))) - lyr2.css(opts.overlayCSS); - lyr2.css('position', full ? 'fixed' : 'absolute'); - - // make iframe layer transparent in IE - if ($.browser.msie || opts.forceIframe) - lyr1.css('opacity',0.0); - - //$([lyr1[0],lyr2[0],lyr3[0]]).appendTo(full ? 'body' : el); - var layers = [lyr1,lyr2,lyr3], $par = full ? $('body') : $(el); - $.each(layers, function() { - this.appendTo($par); - }); - - if (opts.theme && opts.draggable && $.fn.draggable) { - lyr3.draggable({ - handle: '.ui-dialog-titlebar', - cancel: 'li' - }); - } - - // ie7 must use absolute positioning in quirks mode and to account for activex issues (when scrolling) - var expr = setExpr && (!$.boxModel || $('object,embed', full ? null : el).length > 0); - if (ie6 || expr) { - // give body 100% height - if (full && opts.allowBodyStretch && $.boxModel) - $('html,body').css('height','100%'); - - // fix ie6 issue when blocked element has a border width - if ((ie6 || !$.boxModel) && !full) { - var t = sz(el,'borderTopWidth'), l = sz(el,'borderLeftWidth'); - var fixT = t ? '(0 - '+t+')' : 0; - var fixL = l ? '(0 - '+l+')' : 0; - } - - // simulate fixed position - $.each([lyr1,lyr2,lyr3], function(i,o) { - var s = o[0].style; - s.position = 'absolute'; - if (i < 2) { - full ? s.setExpression('height','Math.max(document.body.scrollHeight, document.body.offsetHeight) - (jQuery.boxModel?0:'+opts.quirksmodeOffsetHack+') + "px"') - : s.setExpression('height','this.parentNode.offsetHeight + "px"'); - full ? s.setExpression('width','jQuery.boxModel && document.documentElement.clientWidth || document.body.clientWidth + "px"') - : s.setExpression('width','this.parentNode.offsetWidth + "px"'); - if (fixL) s.setExpression('left', fixL); - if (fixT) s.setExpression('top', fixT); - } - else if (opts.centerY) { - if (full) s.setExpression('top','(document.documentElement.clientHeight || document.body.clientHeight) / 2 - (this.offsetHeight / 2) + (blah = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + "px"'); - s.marginTop = 0; - } - else if (!opts.centerY && full) { - var top = (opts.css && opts.css.top) ? parseInt(opts.css.top) : 0; - var expression = '((document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + '+top+') + "px"'; - s.setExpression('top',expression); - } - }); - } - - // show the message - if (msg) { - if (opts.theme) - lyr3.find('.ui-widget-content').append(msg); - else - lyr3.append(msg); - if (msg.jquery || msg.nodeType) - $(msg).show(); - } - - if (($.browser.msie || opts.forceIframe) && opts.showOverlay) - lyr1.show(); // opacity is zero - if (opts.fadeIn) { - var cb = opts.onBlock ? opts.onBlock : noOp; - var cb1 = (opts.showOverlay && !msg) ? cb : noOp; - var cb2 = msg ? cb : noOp; - if (opts.showOverlay) - lyr2._fadeIn(opts.fadeIn, cb1); - if (msg) - lyr3._fadeIn(opts.fadeIn, cb2); - } - else { - if (opts.showOverlay) - lyr2.show(); - if (msg) - lyr3.show(); - if (opts.onBlock) - opts.onBlock(); - } - - // bind key and mouse events - bind(1, el, opts); - - if (full) { - pageBlock = lyr3[0]; - pageBlockEls = $(':input:enabled:visible',pageBlock); - if (opts.focusInput) - setTimeout(focus, 20); - } - else - center(lyr3[0], opts.centerX, opts.centerY); - - if (opts.timeout) { - // auto-unblock - var to = setTimeout(function() { - full ? $.unblockUI(opts) : $(el).unblock(opts); - }, opts.timeout); - $(el).data('blockUI.timeout', to); - } -}; - -// remove the block -function remove(el, opts) { - var full = (el == window); - var $el = $(el); - var data = $el.data('blockUI.history'); - var to = $el.data('blockUI.timeout'); - if (to) { - clearTimeout(to); - $el.removeData('blockUI.timeout'); - } - opts = $.extend({}, $.blockUI.defaults, opts || {}); - bind(0, el, opts); // unbind events - - if (opts.onUnblock === null) { - opts.onUnblock = $el.data('blockUI.onUnblock'); - $el.removeData('blockUI.onUnblock'); - } - - var els; - if (full) // crazy selector to handle odd field errors in ie6/7 - els = $('body').children().filter('.blockUI').add('body > .blockUI'); - else - els = $('.blockUI', el); - - if (full) - pageBlock = pageBlockEls = null; - - if (opts.fadeOut) { - els.fadeOut(opts.fadeOut); - setTimeout(function() { reset(els,data,opts,el); }, opts.fadeOut); - } - else - reset(els, data, opts, el); -}; - -// move blocking element back into the DOM where it started -function reset(els,data,opts,el) { - els.each(function(i,o) { - // remove via DOM calls so we don't lose event handlers - if (this.parentNode) - this.parentNode.removeChild(this); - }); - - if (data && data.el) { - data.el.style.display = data.display; - data.el.style.position = data.position; - if (data.parent) - data.parent.appendChild(data.el); - $(el).removeData('blockUI.history'); - } - - if (typeof opts.onUnblock == 'function') - opts.onUnblock(el,opts); -}; - -// bind/unbind the handler -function bind(b, el, opts) { - var full = el == window, $el = $(el); - - // don't bother unbinding if there is nothing to unbind - if (!b && (full && !pageBlock || !full && !$el.data('blockUI.isBlocked'))) - return; - if (!full) - $el.data('blockUI.isBlocked', b); - - // don't bind events when overlay is not in use or if bindEvents is false - if (!opts.bindEvents || (b && !opts.showOverlay)) - return; - - // bind anchors and inputs for mouse and key events - var events = 'mousedown mouseup keydown keypress'; - b ? $(document).bind(events, opts, handler) : $(document).unbind(events, handler); - -// former impl... -// var $e = $('a,:input'); -// b ? $e.bind(events, opts, handler) : $e.unbind(events, handler); -}; - -// event handler to suppress keyboard/mouse events when blocking -function handler(e) { - // allow tab navigation (conditionally) - if (e.keyCode && e.keyCode == 9) { - if (pageBlock && e.data.constrainTabKey) { - var els = pageBlockEls; - var fwd = !e.shiftKey && e.target === els[els.length-1]; - var back = e.shiftKey && e.target === els[0]; - if (fwd || back) { - setTimeout(function(){focus(back)},10); - return false; - } - } - } - var opts = e.data; - // allow events within the message content - if ($(e.target).parents('div.' + opts.blockMsgClass).length > 0) - return true; - - // allow events for content that is not being blocked - return $(e.target).parents().children().filter('div.blockUI').length == 0; -}; - -function focus(back) { - if (!pageBlockEls) - return; - var e = pageBlockEls[back===true ? pageBlockEls.length-1 : 0]; - if (e) - e.focus(); -}; - -function center(el, x, y) { - var p = el.parentNode, s = el.style; - var l = ((p.offsetWidth - el.offsetWidth)/2) - sz(p,'borderLeftWidth'); - var t = ((p.offsetHeight - el.offsetHeight)/2) - sz(p,'borderTopWidth'); - if (x) s.left = l > 0 ? (l+'px') : '0'; - if (y) s.top = t > 0 ? (t+'px') : '0'; -}; - -function sz(el, p) { - return parseInt($.css(el,p))||0; -}; - -})(jQuery);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancExplorer/libs/jquery.blockui.js Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,499 @@ +/*! + * jQuery blockUI plugin + * Version 2.39 (23-MAY-2011) + * @requires jQuery v1.2.3 or later + * + * Examples at: http://malsup.com/jquery/block/ + * Copyright (c) 2007-2010 M. Alsup + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * Thanks to Amir-Hossein Sobhi for some excellent contributions! + */ + +;(function($) { + +if (/1\.(0|1|2)\.(0|1|2)/.test($.fn.jquery) || /^1.1/.test($.fn.jquery)) { + alert('blockUI requires jQuery v1.2.3 or later! You are using v' + $.fn.jquery); + return; +} + +$.fn._fadeIn = $.fn.fadeIn; + +var noOp = function() {}; + +// this bit is to ensure we don't call setExpression when we shouldn't (with extra muscle to handle +// retarded userAgent strings on Vista) +var mode = document.documentMode || 0; +var setExpr = $.browser.msie && (($.browser.version < 8 && !mode) || mode < 8); +var ie6 = $.browser.msie && /MSIE 6.0/.test(navigator.userAgent) && !mode; + +// global $ methods for blocking/unblocking the entire page +$.blockUI = function(opts) { install(window, opts); }; +$.unblockUI = function(opts) { remove(window, opts); }; + +// convenience method for quick growl-like notifications (http://www.google.com/search?q=growl) +$.growlUI = function(title, message, timeout, onClose) { + var $m = $('<div class="growlUI"></div>'); + if (title) $m.append('<h1>'+title+'</h1>'); + if (message) $m.append('<h2>'+message+'</h2>'); + if (timeout == undefined) timeout = 3000; + $.blockUI({ + message: $m, fadeIn: 700, fadeOut: 1000, centerY: false, + timeout: timeout, showOverlay: false, + onUnblock: onClose, + css: $.blockUI.defaults.growlCSS + }); +}; + +// plugin method for blocking element content +$.fn.block = function(opts) { + return this.unblock({ fadeOut: 0 }).each(function() { + if ($.css(this,'position') == 'static') + this.style.position = 'relative'; + if ($.browser.msie) + this.style.zoom = 1; // force 'hasLayout' + install(this, opts); + }); +}; + +// plugin method for unblocking element content +$.fn.unblock = function(opts) { + return this.each(function() { + remove(this, opts); + }); +}; + +$.blockUI.version = 2.39; // 2nd generation blocking at no extra cost! + +// override these in your code to change the default behavior and style +$.blockUI.defaults = { + // message displayed when blocking (use null for no message) + message: '<h1>Please wait...</h1>', + + title: null, // title string; only used when theme == true + draggable: true, // only used when theme == true (requires jquery-ui.js to be loaded) + + theme: false, // set to true to use with jQuery UI themes + + // styles for the message when blocking; if you wish to disable + // these and use an external stylesheet then do this in your code: + // $.blockUI.defaults.css = {}; + css: { + padding: 0, + margin: 0, + width: '30%', + top: '40%', + left: '35%', + textAlign: 'center', + color: '#000', + border: '3px solid #aaa', + backgroundColor:'#fff', + cursor: 'wait' + }, + + // minimal style set used when themes are used + themedCSS: { + width: '30%', + top: '40%', + left: '35%' + }, + + // styles for the overlay + overlayCSS: { + backgroundColor: '#000', + opacity: 0.6, + cursor: 'wait' + }, + + // styles applied when using $.growlUI + growlCSS: { + width: '350px', + top: '10px', + left: '', + right: '10px', + border: 'none', + padding: '5px', + opacity: 0.6, + cursor: 'default', + color: '#fff', + backgroundColor: '#000', + '-webkit-border-radius': '10px', + '-moz-border-radius': '10px', + 'border-radius': '10px' + }, + + // IE issues: 'about:blank' fails on HTTPS and javascript:false is s-l-o-w + // (hat tip to Jorge H. N. de Vasconcelos) + iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank', + + // force usage of iframe in non-IE browsers (handy for blocking applets) + forceIframe: false, + + // z-index for the blocking overlay + baseZ: 1000, + + // set these to true to have the message automatically centered + centerX: true, // <-- only effects element blocking (page block controlled via css above) + centerY: true, + + // allow body element to be stetched in ie6; this makes blocking look better + // on "short" pages. disable if you wish to prevent changes to the body height + allowBodyStretch: true, + + // enable if you want key and mouse events to be disabled for content that is blocked + bindEvents: true, + + // be default blockUI will supress tab navigation from leaving blocking content + // (if bindEvents is true) + constrainTabKey: true, + + // fadeIn time in millis; set to 0 to disable fadeIn on block + fadeIn: 200, + + // fadeOut time in millis; set to 0 to disable fadeOut on unblock + fadeOut: 400, + + // time in millis to wait before auto-unblocking; set to 0 to disable auto-unblock + timeout: 0, + + // disable if you don't want to show the overlay + showOverlay: true, + + // if true, focus will be placed in the first available input field when + // page blocking + focusInput: true, + + // suppresses the use of overlay styles on FF/Linux (due to performance issues with opacity) + applyPlatformOpacityRules: true, + + // callback method invoked when fadeIn has completed and blocking message is visible + onBlock: null, + + // callback method invoked when unblocking has completed; the callback is + // passed the element that has been unblocked (which is the window object for page + // blocks) and the options that were passed to the unblock call: + // onUnblock(element, options) + onUnblock: null, + + // don't ask; if you really must know: http://groups.google.com/group/jquery-en/browse_thread/thread/36640a8730503595/2f6a79a77a78e493#2f6a79a77a78e493 + quirksmodeOffsetHack: 4, + + // class name of the message block + blockMsgClass: 'blockMsg' +}; + +// private data and functions follow... + +var pageBlock = null; +var pageBlockEls = []; + +function install(el, opts) { + var full = (el == window); + var msg = opts && opts.message !== undefined ? opts.message : undefined; + opts = $.extend({}, $.blockUI.defaults, opts || {}); + opts.overlayCSS = $.extend({}, $.blockUI.defaults.overlayCSS, opts.overlayCSS || {}); + var css = $.extend({}, $.blockUI.defaults.css, opts.css || {}); + var themedCSS = $.extend({}, $.blockUI.defaults.themedCSS, opts.themedCSS || {}); + msg = msg === undefined ? opts.message : msg; + + // remove the current block (if there is one) + if (full && pageBlock) + remove(window, {fadeOut:0}); + + // if an existing element is being used as the blocking content then we capture + // its current place in the DOM (and current display style) so we can restore + // it when we unblock + if (msg && typeof msg != 'string' && (msg.parentNode || msg.jquery)) { + var node = msg.jquery ? msg[0] : msg; + var data = {}; + $(el).data('blockUI.history', data); + data.el = node; + data.parent = node.parentNode; + data.display = node.style.display; + data.position = node.style.position; + if (data.parent) + data.parent.removeChild(node); + } + + $(el).data('blockUI.onUnblock', opts.onUnblock); + var z = opts.baseZ; + + // blockUI uses 3 layers for blocking, for simplicity they are all used on every platform; + // layer1 is the iframe layer which is used to supress bleed through of underlying content + // layer2 is the overlay layer which has opacity and a wait cursor (by default) + // layer3 is the message content that is displayed while blocking + + var lyr1 = ($.browser.msie || opts.forceIframe) + ? $('<iframe class="blockUI" style="z-index:'+ (z++) +';display:none;border:none;margin:0;padding:0;position:absolute;width:100%;height:100%;top:0;left:0" src="'+opts.iframeSrc+'"></iframe>') + : $('<div class="blockUI" style="display:none"></div>'); + + var lyr2 = opts.theme + ? $('<div class="blockUI blockOverlay ui-widget-overlay" style="z-index:'+ (z++) +';display:none"></div>') + : $('<div class="blockUI blockOverlay" style="z-index:'+ (z++) +';display:none;border:none;margin:0;padding:0;width:100%;height:100%;top:0;left:0"></div>'); + + var lyr3, s; + if (opts.theme && full) { + s = '<div class="blockUI ' + opts.blockMsgClass + ' blockPage ui-dialog ui-widget ui-corner-all" style="z-index:'+(z+10)+';display:none;position:fixed">' + + '<div class="ui-widget-header ui-dialog-titlebar ui-corner-all blockTitle">'+(opts.title || ' ')+'</div>' + + '<div class="ui-widget-content ui-dialog-content"></div>' + + '</div>'; + } + else if (opts.theme) { + s = '<div class="blockUI ' + opts.blockMsgClass + ' blockElement ui-dialog ui-widget ui-corner-all" style="z-index:'+(z+10)+';display:none;position:absolute">' + + '<div class="ui-widget-header ui-dialog-titlebar ui-corner-all blockTitle">'+(opts.title || ' ')+'</div>' + + '<div class="ui-widget-content ui-dialog-content"></div>' + + '</div>'; + } + else if (full) { + s = '<div class="blockUI ' + opts.blockMsgClass + ' blockPage" style="z-index:'+(z+10)+';display:none;position:fixed"></div>'; + } + else { + s = '<div class="blockUI ' + opts.blockMsgClass + ' blockElement" style="z-index:'+(z+10)+';display:none;position:absolute"></div>'; + } + lyr3 = $(s); + + // if we have a message, style it + if (msg) { + if (opts.theme) { + lyr3.css(themedCSS); + lyr3.addClass('ui-widget-content'); + } + else + lyr3.css(css); + } + + // style the overlay + if (!opts.theme && (!opts.applyPlatformOpacityRules || !($.browser.mozilla && /Linux/.test(navigator.platform)))) + lyr2.css(opts.overlayCSS); + lyr2.css('position', full ? 'fixed' : 'absolute'); + + // make iframe layer transparent in IE + if ($.browser.msie || opts.forceIframe) + lyr1.css('opacity',0.0); + + //$([lyr1[0],lyr2[0],lyr3[0]]).appendTo(full ? 'body' : el); + var layers = [lyr1,lyr2,lyr3], $par = full ? $('body') : $(el); + $.each(layers, function() { + this.appendTo($par); + }); + + if (opts.theme && opts.draggable && $.fn.draggable) { + lyr3.draggable({ + handle: '.ui-dialog-titlebar', + cancel: 'li' + }); + } + + // ie7 must use absolute positioning in quirks mode and to account for activex issues (when scrolling) + var expr = setExpr && (!$.boxModel || $('object,embed', full ? null : el).length > 0); + if (ie6 || expr) { + // give body 100% height + if (full && opts.allowBodyStretch && $.boxModel) + $('html,body').css('height','100%'); + + // fix ie6 issue when blocked element has a border width + if ((ie6 || !$.boxModel) && !full) { + var t = sz(el,'borderTopWidth'), l = sz(el,'borderLeftWidth'); + var fixT = t ? '(0 - '+t+')' : 0; + var fixL = l ? '(0 - '+l+')' : 0; + } + + // simulate fixed position + $.each([lyr1,lyr2,lyr3], function(i,o) { + var s = o[0].style; + s.position = 'absolute'; + if (i < 2) { + full ? s.setExpression('height','Math.max(document.body.scrollHeight, document.body.offsetHeight) - (jQuery.boxModel?0:'+opts.quirksmodeOffsetHack+') + "px"') + : s.setExpression('height','this.parentNode.offsetHeight + "px"'); + full ? s.setExpression('width','jQuery.boxModel && document.documentElement.clientWidth || document.body.clientWidth + "px"') + : s.setExpression('width','this.parentNode.offsetWidth + "px"'); + if (fixL) s.setExpression('left', fixL); + if (fixT) s.setExpression('top', fixT); + } + else if (opts.centerY) { + if (full) s.setExpression('top','(document.documentElement.clientHeight || document.body.clientHeight) / 2 - (this.offsetHeight / 2) + (blah = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + "px"'); + s.marginTop = 0; + } + else if (!opts.centerY && full) { + var top = (opts.css && opts.css.top) ? parseInt(opts.css.top) : 0; + var expression = '((document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + '+top+') + "px"'; + s.setExpression('top',expression); + } + }); + } + + // show the message + if (msg) { + if (opts.theme) + lyr3.find('.ui-widget-content').append(msg); + else + lyr3.append(msg); + if (msg.jquery || msg.nodeType) + $(msg).show(); + } + + if (($.browser.msie || opts.forceIframe) && opts.showOverlay) + lyr1.show(); // opacity is zero + if (opts.fadeIn) { + var cb = opts.onBlock ? opts.onBlock : noOp; + var cb1 = (opts.showOverlay && !msg) ? cb : noOp; + var cb2 = msg ? cb : noOp; + if (opts.showOverlay) + lyr2._fadeIn(opts.fadeIn, cb1); + if (msg) + lyr3._fadeIn(opts.fadeIn, cb2); + } + else { + if (opts.showOverlay) + lyr2.show(); + if (msg) + lyr3.show(); + if (opts.onBlock) + opts.onBlock(); + } + + // bind key and mouse events + bind(1, el, opts); + + if (full) { + pageBlock = lyr3[0]; + pageBlockEls = $(':input:enabled:visible',pageBlock); + if (opts.focusInput) + setTimeout(focus, 20); + } + else + center(lyr3[0], opts.centerX, opts.centerY); + + if (opts.timeout) { + // auto-unblock + var to = setTimeout(function() { + full ? $.unblockUI(opts) : $(el).unblock(opts); + }, opts.timeout); + $(el).data('blockUI.timeout', to); + } +}; + +// remove the block +function remove(el, opts) { + var full = (el == window); + var $el = $(el); + var data = $el.data('blockUI.history'); + var to = $el.data('blockUI.timeout'); + if (to) { + clearTimeout(to); + $el.removeData('blockUI.timeout'); + } + opts = $.extend({}, $.blockUI.defaults, opts || {}); + bind(0, el, opts); // unbind events + + if (opts.onUnblock === null) { + opts.onUnblock = $el.data('blockUI.onUnblock'); + $el.removeData('blockUI.onUnblock'); + } + + var els; + if (full) // crazy selector to handle odd field errors in ie6/7 + els = $('body').children().filter('.blockUI').add('body > .blockUI'); + else + els = $('.blockUI', el); + + if (full) + pageBlock = pageBlockEls = null; + + if (opts.fadeOut) { + els.fadeOut(opts.fadeOut); + setTimeout(function() { reset(els,data,opts,el); }, opts.fadeOut); + } + else + reset(els, data, opts, el); +}; + +// move blocking element back into the DOM where it started +function reset(els,data,opts,el) { + els.each(function(i,o) { + // remove via DOM calls so we don't lose event handlers + if (this.parentNode) + this.parentNode.removeChild(this); + }); + + if (data && data.el) { + data.el.style.display = data.display; + data.el.style.position = data.position; + if (data.parent) + data.parent.appendChild(data.el); + $(el).removeData('blockUI.history'); + } + + if (typeof opts.onUnblock == 'function') + opts.onUnblock(el,opts); +}; + +// bind/unbind the handler +function bind(b, el, opts) { + var full = el == window, $el = $(el); + + // don't bother unbinding if there is nothing to unbind + if (!b && (full && !pageBlock || !full && !$el.data('blockUI.isBlocked'))) + return; + if (!full) + $el.data('blockUI.isBlocked', b); + + // don't bind events when overlay is not in use or if bindEvents is false + if (!opts.bindEvents || (b && !opts.showOverlay)) + return; + + // bind anchors and inputs for mouse and key events + var events = 'mousedown mouseup keydown keypress'; + b ? $(document).bind(events, opts, handler) : $(document).unbind(events, handler); + +// former impl... +// var $e = $('a,:input'); +// b ? $e.bind(events, opts, handler) : $e.unbind(events, handler); +}; + +// event handler to suppress keyboard/mouse events when blocking +function handler(e) { + // allow tab navigation (conditionally) + if (e.keyCode && e.keyCode == 9) { + if (pageBlock && e.data.constrainTabKey) { + var els = pageBlockEls; + var fwd = !e.shiftKey && e.target === els[els.length-1]; + var back = e.shiftKey && e.target === els[0]; + if (fwd || back) { + setTimeout(function(){focus(back)},10); + return false; + } + } + } + var opts = e.data; + // allow events within the message content + if ($(e.target).parents('div.' + opts.blockMsgClass).length > 0) + return true; + + // allow events for content that is not being blocked + return $(e.target).parents().children().filter('div.blockUI').length == 0; +}; + +function focus(back) { + if (!pageBlockEls) + return; + var e = pageBlockEls[back===true ? pageBlockEls.length-1 : 0]; + if (e) + e.focus(); +}; + +function center(el, x, y) { + var p = el.parentNode, s = el.style; + var l = ((p.offsetWidth - el.offsetWidth)/2) - sz(p,'borderLeftWidth'); + var t = ((p.offsetHeight - el.offsetHeight)/2) - sz(p,'borderTopWidth'); + if (x) s.left = l > 0 ? (l+'px') : '0'; + if (y) s.top = t > 0 ? (t+'px') : '0'; +}; + +function sz(el, p) { + return parseInt($.css(el,p))||0; +}; + +})(jQuery);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/DatabaseWrapper.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,853 @@ +/** + * 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 "DatabaseWrapper.h" + +#include "../Core/DicomFormat/DicomArray.h" +#include "../Core/Uuid.h" +#include "EmbeddedResources.h" + +#include <glog/logging.h> +#include <stdio.h> + +namespace Orthanc +{ + + namespace Internals + { + class SignalFileDeleted : public SQLite::IScalarFunction + { + private: + IServerIndexListener& listener_; + + public: + SignalFileDeleted(IServerIndexListener& listener) : + listener_(listener) + { + } + + virtual const char* GetName() const + { + return "SignalFileDeleted"; + } + + virtual unsigned int GetCardinality() const + { + return 5; + } + + virtual void Compute(SQLite::FunctionContext& context) + { + 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); + } + }; + + class SignalRemainingAncestor : public SQLite::IScalarFunction + { + private: + bool hasRemainingAncestor_; + std::string remainingPublicId_; + ResourceType remainingType_; + + public: + void Reset() + { + hasRemainingAncestor_ = false; + } + + virtual const char* GetName() const + { + return "SignalRemainingAncestor"; + } + + virtual unsigned int GetCardinality() const + { + return 2; + } + + virtual void Compute(SQLite::FunctionContext& context) + { + VLOG(1) << "There exists a remaining ancestor with public ID \"" + << context.GetStringValue(0) + << "\" of type " + << context.GetIntValue(1); + + if (!hasRemainingAncestor_ || + remainingType_ >= context.GetIntValue(1)) + { + hasRemainingAncestor_ = true; + remainingPublicId_ = context.GetStringValue(0); + remainingType_ = static_cast<ResourceType>(context.GetIntValue(1)); + } + } + + bool HasRemainingAncestor() const + { + return hasRemainingAncestor_; + } + + const std::string& GetRemainingAncestorId() const + { + assert(hasRemainingAncestor_); + return remainingPublicId_; + } + + ResourceType GetRemainingAncestorType() const + { + assert(hasRemainingAncestor_); + return remainingType_; + } + }; + } + + + + void DatabaseWrapper::SetGlobalProperty(GlobalProperty property, + const std::string& value) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO GlobalProperties VALUES(?, ?)"); + s.BindInt(0, property); + s.BindString(1, value); + s.Run(); + } + + bool DatabaseWrapper::LookupGlobalProperty(std::string& target, + GlobalProperty property) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT value FROM GlobalProperties WHERE property=?"); + s.BindInt(0, property); + + if (!s.Step()) + { + return false; + } + else + { + target = s.ColumnString(0); + return true; + } + } + + std::string DatabaseWrapper::GetGlobalProperty(GlobalProperty property, + const std::string& defaultValue) + { + std::string s; + if (LookupGlobalProperty(s, property)) + { + return s; + } + else + { + return defaultValue; + } + } + + int64_t DatabaseWrapper::CreateResource(const std::string& publicId, + ResourceType type) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Resources VALUES(NULL, ?, ?, NULL)"); + s.BindInt(0, type); + s.BindString(1, publicId); + s.Run(); + int64_t id = db_.GetLastInsertRowId(); + + ChangeType changeType; + switch (type) + { + case ResourceType_Patient: + changeType = ChangeType_NewPatient; + break; + + case ResourceType_Study: + changeType = ChangeType_NewStudy; + break; + + case ResourceType_Series: + changeType = ChangeType_NewSeries; + break; + + case ResourceType_Instance: + changeType = ChangeType_NewInstance; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + LogChange(changeType, id, type); + return id; + } + + bool DatabaseWrapper::LookupResource(const std::string& publicId, + int64_t& id, + ResourceType& type) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT internalId, resourceType FROM Resources WHERE publicId=?"); + s.BindString(0, publicId); + + if (!s.Step()) + { + return false; + } + else + { + id = s.ColumnInt(0); + type = static_cast<ResourceType>(s.ColumnInt(1)); + + // Check whether there is a single resource with this public id + assert(!s.Step()); + + return true; + } + } + + bool DatabaseWrapper::LookupParent(int64_t& parentId, + int64_t resourceId) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT parentId FROM Resources WHERE internalId=?"); + s.BindInt(0, resourceId); + + if (!s.Step()) + { + throw OrthancException(ErrorCode_UnknownResource); + } + + if (s.ColumnIsNull(0)) + { + return false; + } + else + { + parentId = s.ColumnInt(0); + return true; + } + } + + std::string DatabaseWrapper::GetPublicId(int64_t resourceId) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT publicId FROM Resources WHERE internalId=?"); + s.BindInt(0, resourceId); + + if (!s.Step()) + { + throw OrthancException(ErrorCode_UnknownResource); + } + + return s.ColumnString(0); + } + + void DatabaseWrapper::AttachChild(int64_t parent, + int64_t child) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "UPDATE Resources SET parentId = ? WHERE internalId = ?"); + s.BindInt(0, parent); + s.BindInt(1, child); + s.Run(); + } + + void DatabaseWrapper::GetChildren(Json::Value& childrenPublicIds, + int64_t id) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE parentId=?"); + s.BindInt(0, id); + + childrenPublicIds = Json::arrayValue; + while (s.Step()) + { + childrenPublicIds.append(s.ColumnString(0)); + } + } + + + void DatabaseWrapper::DeleteResource(int64_t id) + { + signalRemainingAncestor_->Reset(); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Resources WHERE internalId=?"); + s.BindInt(0, id); + s.Run(); + + if (signalRemainingAncestor_->HasRemainingAncestor()) + { + listener_.SignalRemainingAncestor(signalRemainingAncestor_->GetRemainingAncestorType(), + signalRemainingAncestor_->GetRemainingAncestorId()); + } + } + + void DatabaseWrapper::SetMetadata(int64_t id, + MetadataType type, + const std::string& value) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata VALUES(?, ?, ?)"); + s.BindInt(0, id); + s.BindInt(1, type); + s.BindString(2, value); + s.Run(); + } + + bool DatabaseWrapper::LookupMetadata(std::string& target, + int64_t id, + MetadataType type) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT value FROM Metadata WHERE id=? AND type=?"); + s.BindInt(0, id); + s.BindInt(1, type); + + if (!s.Step()) + { + return false; + } + else + { + target = s.ColumnString(0); + return true; + } + } + + std::string DatabaseWrapper::GetMetadata(int64_t id, + MetadataType type, + const std::string& defaultValue) + { + std::string s; + if (LookupMetadata(s, id, type)) + { + return s; + } + else + { + return defaultValue; + } + } + + + bool DatabaseWrapper::GetMetadataAsInteger(int& result, + int64_t id, + MetadataType type) + { + std::string s = GetMetadata(id, type, ""); + if (s.size() == 0) + { + return false; + } + + try + { + result = boost::lexical_cast<int>(s); + return true; + } + catch (boost::bad_lexical_cast&) + { + return false; + } + } + + + + void DatabaseWrapper::AddAttachment(int64_t id, + const FileInfo& attachment) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles VALUES(?, ?, ?, ?, ?, ?)"); + s.BindInt(0, id); + s.BindInt(1, attachment.GetContentType()); + s.BindString(2, attachment.GetUuid()); + s.BindInt(3, attachment.GetCompressedSize()); + s.BindInt(4, attachment.GetUncompressedSize()); + s.BindInt(5, attachment.GetCompressionType()); + s.Run(); + } + + bool DatabaseWrapper::LookupAttachment(FileInfo& attachment, + int64_t id, + FileContentType contentType) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT uuid, uncompressedSize, compressionType, compressedSize FROM AttachedFiles WHERE id=? AND fileType=?"); + s.BindInt(0, id); + s.BindInt(1, contentType); + + if (!s.Step()) + { + return false; + } + else + { + attachment = FileInfo(s.ColumnString(0), + contentType, + s.ColumnInt(1), + static_cast<CompressionType>(s.ColumnInt(2)), + s.ColumnInt(3)); + return true; + } + } + + void DatabaseWrapper::SetMainDicomTags(int64_t id, + const DicomMap& tags) + { + DicomArray flattened(tags); + for (size_t i = 0; i < flattened.GetSize(); i++) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)"); + s.BindInt(0, id); + s.BindInt(1, flattened.GetElement(i).GetTag().GetGroup()); + s.BindInt(2, flattened.GetElement(i).GetTag().GetElement()); + s.BindString(3, flattened.GetElement(i).GetValue().AsString()); + s.Run(); + } + } + + void DatabaseWrapper::GetMainDicomTags(DicomMap& map, + int64_t id) + { + map.Clear(); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM MainDicomTags WHERE id=?"); + s.BindInt(0, id); + while (s.Step()) + { + map.SetValue(s.ColumnInt(1), + s.ColumnInt(2), + s.ColumnString(3)); + } + } + + + bool DatabaseWrapper::GetParentPublicId(std::string& result, + int64_t id) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b " + "WHERE a.internalId = b.parentId AND b.internalId = ?"); + s.BindInt(0, id); + + if (s.Step()) + { + result = s.ColumnString(0); + return true; + } + else + { + return false; + } + } + + + void DatabaseWrapper::GetChildrenPublicId(std::list<std::string>& result, + int64_t id) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b " + "WHERE a.parentId = b.internalId AND b.internalId = ?"); + s.BindInt(0, id); + + result.clear(); + + while (s.Step()) + { + result.push_back(s.ColumnString(0)); + } + } + + + void DatabaseWrapper::GetChildrenInternalId(std::list<int64_t>& result, + int64_t id) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.internalId FROM Resources AS a, Resources AS b " + "WHERE a.parentId = b.internalId AND b.internalId = ?"); + s.BindInt(0, id); + + result.clear(); + + while (s.Step()) + { + result.push_back(s.ColumnInt(0)); + } + } + + + void DatabaseWrapper::LogChange(ChangeType changeType, + int64_t internalId, + ResourceType resourceType, + const boost::posix_time::ptime& date) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Changes VALUES(NULL, ?, ?, ?, ?)"); + s.BindInt(0, changeType); + s.BindInt(1, internalId); + s.BindInt(2, resourceType); + s.BindString(3, boost::posix_time::to_iso_string(date)); + s.Run(); + } + + + void DatabaseWrapper::GetChangesInternal(Json::Value& target, + SQLite::Statement& s, + int64_t since, + unsigned int maxResults) + { + Json::Value changes = Json::arrayValue; + int64_t last = since; + + while (changes.size() < maxResults && s.Step()) + { + int64_t seq = s.ColumnInt(0); + ChangeType changeType = static_cast<ChangeType>(s.ColumnInt(1)); + int64_t internalId = s.ColumnInt(2); + ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(3)); + const std::string& date = s.ColumnString(4); + std::string publicId = GetPublicId(internalId); + + Json::Value item = Json::objectValue; + item["Seq"] = static_cast<int>(seq); + item["ChangeType"] = ToString(changeType); + item["ResourceType"] = ToString(resourceType); + item["ID"] = publicId; + item["Path"] = GetBasePath(resourceType, publicId); + item["Date"] = date; + last = seq; + + changes.append(item); + } + + target = Json::objectValue; + target["Changes"] = changes; + target["Done"] = !(changes.size() == maxResults && s.Step()); + target["Last"] = static_cast<int>(last); + } + + + void DatabaseWrapper::GetChanges(Json::Value& target, + int64_t since, + unsigned int maxResults) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes WHERE seq>? ORDER BY seq LIMIT ?"); + s.BindInt(0, since); + s.BindInt(1, maxResults + 1); + GetChangesInternal(target, s, since, maxResults); + } + + void DatabaseWrapper::GetLastChange(Json::Value& target) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes ORDER BY seq DESC LIMIT 1"); + GetChangesInternal(target, s, 0, 1); + } + + + void DatabaseWrapper::LogExportedResource(ResourceType resourceType, + const std::string& publicId, + const std::string& remoteModality, + const std::string& patientId, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + const std::string& sopInstanceUid, + const boost::posix_time::ptime& date) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "INSERT INTO ExportedResources VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?)"); + + s.BindInt(0, resourceType); + s.BindString(1, publicId); + s.BindString(2, remoteModality); + s.BindString(3, patientId); + s.BindString(4, studyInstanceUid); + s.BindString(5, seriesInstanceUid); + s.BindString(6, sopInstanceUid); + s.BindString(7, boost::posix_time::to_iso_string(date)); + + s.Run(); + } + + + void DatabaseWrapper::GetExportedResources(Json::Value& target, + SQLite::Statement& s, + int64_t since, + unsigned int maxResults) + { + Json::Value changes = Json::arrayValue; + int64_t last = since; + + while (changes.size() < maxResults && s.Step()) + { + int64_t seq = s.ColumnInt(0); + ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(1)); + std::string publicId = s.ColumnString(2); + + Json::Value item = Json::objectValue; + item["Seq"] = static_cast<int>(seq); + item["ResourceType"] = ToString(resourceType); + item["ID"] = publicId; + item["Path"] = GetBasePath(resourceType, publicId); + item["RemoteModality"] = s.ColumnString(3); + item["Date"] = s.ColumnString(8); + + // WARNING: Do not add "break" below and do not reorder the case items! + switch (resourceType) + { + case ResourceType_Instance: + item["SopInstanceUid"] = s.ColumnString(7); + + case ResourceType_Series: + item["SeriesInstanceUid"] = s.ColumnString(6); + + case ResourceType_Study: + item["StudyInstanceUid"] = s.ColumnString(5); + + case ResourceType_Patient: + item["PatientId"] = s.ColumnString(4); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + last = seq; + + changes.append(item); + } + + target = Json::objectValue; + target["Exports"] = changes; + target["Done"] = !(changes.size() == maxResults && s.Step()); + target["Last"] = static_cast<int>(last); + } + + + void DatabaseWrapper::GetExportedResources(Json::Value& target, + int64_t since, + unsigned int maxResults) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT * FROM ExportedResources WHERE seq>? ORDER BY seq LIMIT ?"); + s.BindInt(0, since); + s.BindInt(1, maxResults + 1); + GetExportedResources(target, s, since, maxResults); + } + + + void DatabaseWrapper::GetLastExportedResource(Json::Value& target) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT * FROM ExportedResources ORDER BY seq DESC LIMIT 1"); + GetExportedResources(target, s, 0, 1); + } + + + + + int64_t DatabaseWrapper::GetTableRecordCount(const std::string& table) + { + char buf[128]; + sprintf(buf, "SELECT COUNT(*) FROM %s", table.c_str()); + SQLite::Statement s(db_, buf); + + if (!s.Step()) + { + throw OrthancException(ErrorCode_InternalError); + } + + int64_t c = s.ColumnInt(0); + assert(!s.Step()); + + return c; + } + + + uint64_t DatabaseWrapper::GetTotalCompressedSize() + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles"); + s.Run(); + return static_cast<uint64_t>(s.ColumnInt64(0)); + } + + + uint64_t DatabaseWrapper::GetTotalUncompressedSize() + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles"); + s.Run(); + return static_cast<uint64_t>(s.ColumnInt64(0)); + } + + void DatabaseWrapper::GetAllPublicIds(Json::Value& target, + ResourceType resourceType) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE resourceType=?"); + s.BindInt(0, resourceType); + + target = Json::arrayValue; + while (s.Step()) + { + target.append(s.ColumnString(0)); + } + } + + + DatabaseWrapper::DatabaseWrapper(const std::string& path, + IServerIndexListener& listener) : + listener_(listener) + { + db_.Open(path); + Open(); + } + + DatabaseWrapper::DatabaseWrapper(IServerIndexListener& listener) : + listener_(listener) + { + db_.OpenInMemory(); + Open(); + } + + void DatabaseWrapper::Open() + { + if (!db_.DoesTableExist("GlobalProperties")) + { + LOG(INFO) << "Creating the database"; + std::string query; + EmbeddedResources::GetFileResource(query, EmbeddedResources::PREPARE_DATABASE); + db_.Execute(query); + } + + // Sanity check of the version of the database + std::string version = GetGlobalProperty(GlobalProperty_DatabaseSchemaVersion, "Unknown"); + bool ok = false; + try + { + 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 3 of + // the DB schema (since Orthanc 0.3.2) + ok = (v == 3); + } + catch (boost::bad_lexical_cast&) + { + } + + if (!ok) + { + throw OrthancException(ErrorCode_IncompatibleDatabaseVersion); + } + + signalRemainingAncestor_ = new Internals::SignalRemainingAncestor; + db_.Register(signalRemainingAncestor_); + db_.Register(new Internals::SignalFileDeleted(listener_)); + } + + uint64_t DatabaseWrapper::GetResourceCount(ResourceType resourceType) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT COUNT(*) FROM Resources WHERE resourceType=?"); + s.BindInt(0, resourceType); + + if (!s.Step()) + { + throw OrthancException(ErrorCode_InternalError); + } + + int64_t c = s.ColumnInt(0); + assert(!s.Step()); + + 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 + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/DatabaseWrapper.h Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,212 @@ +/** + * 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 "../Core/SQLite/Connection.h" +#include "../Core/SQLite/Transaction.h" +#include "../Core/DicomFormat/DicomInstanceHasher.h" +#include "../Core/FileStorage/FileInfo.h" +#include "IServerIndexListener.h" + +#include <list> +#include <boost/date_time/posix_time/posix_time.hpp> + +namespace Orthanc +{ + namespace Internals + { + class SignalRemainingAncestor; + } + + /** + * This class manages an instance of the Orthanc SQLite database. It + * translates low-level requests into SQL statements. Mutual + * exclusion MUST be implemented at a higher level. + **/ + class DatabaseWrapper + { + private: + IServerIndexListener& listener_; + SQLite::Connection db_; + Internals::SignalRemainingAncestor* signalRemainingAncestor_; + + void Open(); + + void GetChangesInternal(Json::Value& target, + SQLite::Statement& s, + int64_t since, + unsigned int maxResults); + + void GetExportedResources(Json::Value& target, + SQLite::Statement& s, + int64_t since, + unsigned int maxResults); + + public: + void SetGlobalProperty(GlobalProperty property, + const std::string& value); + + bool LookupGlobalProperty(std::string& target, + GlobalProperty property); + + std::string GetGlobalProperty(GlobalProperty property, + const std::string& defaultValue = ""); + + int64_t CreateResource(const std::string& publicId, + ResourceType type); + + bool LookupResource(const std::string& publicId, + int64_t& id, + ResourceType& type); + + bool LookupParent(int64_t& parentId, + int64_t resourceId); + + std::string GetPublicId(int64_t resourceId); + + void AttachChild(int64_t parent, + int64_t child); + + void GetChildren(Json::Value& childrenPublicIds, + int64_t id); + + void DeleteResource(int64_t id); + + void SetMetadata(int64_t id, + MetadataType type, + const std::string& value); + + bool LookupMetadata(std::string& target, + int64_t id, + MetadataType type); + + std::string GetMetadata(int64_t id, + MetadataType type, + const std::string& defaultValue = ""); + + bool GetMetadataAsInteger(int& result, + int64_t id, + MetadataType type); + + void AddAttachment(int64_t id, + const FileInfo& attachment); + + bool LookupAttachment(FileInfo& attachment, + int64_t id, + FileContentType contentType); + + void SetMainDicomTags(int64_t id, + const DicomMap& tags); + + void GetMainDicomTags(DicomMap& map, + int64_t id); + + bool GetParentPublicId(std::string& result, + int64_t id); + + void GetChildrenPublicId(std::list<std::string>& result, + int64_t id); + + void GetChildrenInternalId(std::list<int64_t>& result, + int64_t id); + + void LogChange(ChangeType changeType, + int64_t internalId, + ResourceType resourceType, + const boost::posix_time::ptime& date = boost::posix_time::second_clock::local_time()); + + void GetChanges(Json::Value& target, + int64_t since, + unsigned int maxResults); + + void GetLastChange(Json::Value& target); + + void LogExportedResource(ResourceType resourceType, + const std::string& publicId, + const std::string& remoteModality, + const std::string& patientId, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + const std::string& sopInstanceUid, + const boost::posix_time::ptime& date = + boost::posix_time::second_clock::local_time()); + + void GetExportedResources(Json::Value& target, + int64_t since, + unsigned int maxResults); + + void GetLastExportedResource(Json::Value& target); + + // For unit testing only! + int64_t GetTableRecordCount(const std::string& table); + + uint64_t GetTotalCompressedSize(); + + uint64_t GetTotalUncompressedSize(); + + uint64_t GetResourceCount(ResourceType resourceType); + + 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); + + DatabaseWrapper(IServerIndexListener& listener); + + SQLite::Transaction* StartTransaction() + { + return new SQLite::Transaction(db_); + } + + const char* GetErrorMessage() const + { + return db_.GetErrorMessage(); + } + + void FlushToDisk() + { + db_.FlushToDisk(); + } + }; +}
--- a/OrthancServer/DicomProtocol/DicomFindAnswers.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/DicomProtocol/DicomFindAnswers.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/OrthancServer/DicomProtocol/DicomFindAnswers.h Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/DicomProtocol/DicomFindAnswers.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/OrthancServer/DicomProtocol/DicomServer.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/DicomProtocol/DicomServer.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -47,7 +59,7 @@ /* make sure data dictionary is loaded */ if (!dcmDataDict.isDictionaryLoaded()) { - LOG(WARNING) << "no data dictionary loaded, check environment variable: " << DCM_DICT_ENVIRONMENT_VARIABLE; + LOG(ERROR) << "no data dictionary loaded, check environment variable: " << DCM_DICT_ENVIRONMENT_VARIABLE; } /* initialize network, i.e. create an instance of T_ASC_Network*. */ @@ -60,7 +72,7 @@ throw OrthancException("Cannot create network"); } - LOG(WARNING) << "DICOM server started"; + LOG(INFO) << "DICOM server started"; server->started_ = true; @@ -83,7 +95,7 @@ } } - LOG(WARNING) << "DICOM server stopping"; + LOG(INFO) << "DICOM server stopping"; /* drop the network, i.e. free memory of T_ASC_Network* structure. This call */ /* is the counterpart of ASC_initializeNetwork(...) which was called above. */
--- a/OrthancServer/DicomProtocol/DicomServer.h Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/DicomProtocol/DicomServer.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/OrthancServer/DicomProtocol/DicomUserConnection.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/DicomProtocol/DicomUserConnection.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -24,8 +36,6 @@ #include "../ToDcmtkBridge.h" #include "../FromDcmtkBridge.h" -#include <dcmtk/dcmnet/assoc.h> -#include <dcmtk/dcmnet/dimse.h> #include <dcmtk/dcmdata/dcistrmb.h> #include <dcmtk/dcmdata/dcistrmf.h> #include <dcmtk/dcmdata/dcfilefo.h>
--- a/OrthancServer/DicomProtocol/DicomUserConnection.h Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/DicomProtocol/DicomUserConnection.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/OrthancServer/DicomProtocol/IApplicationEntityFilter.h Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/DicomProtocol/IApplicationEntityFilter.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/OrthancServer/DicomProtocol/IFindRequestHandler.h Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/DicomProtocol/IFindRequestHandler.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/OrthancServer/DicomProtocol/IFindRequestHandlerFactory.h Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/DicomProtocol/IFindRequestHandlerFactory.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/OrthancServer/DicomProtocol/IMoveRequestHandler.h Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/DicomProtocol/IMoveRequestHandler.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/OrthancServer/DicomProtocol/IMoveRequestHandlerFactory.h Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/DicomProtocol/IMoveRequestHandlerFactory.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/OrthancServer/DicomProtocol/IStoreRequestHandler.h Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/DicomProtocol/IStoreRequestHandler.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/OrthancServer/DicomProtocol/IStoreRequestHandlerFactory.h Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/DicomProtocol/IStoreRequestHandlerFactory.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/OrthancServer/FromDcmtkBridge.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/FromDcmtkBridge.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -37,21 +49,207 @@ #include <dcmtk/dcmdata/dcdicent.h> #include <dcmtk/dcmdata/dcdict.h> -#include <dcmtk/dcmdata/dcelem.h> #include <dcmtk/dcmdata/dcfilefo.h> #include <dcmtk/dcmdata/dcistrmb.h> -#include <dcmtk/dcmdata/dcsequen.h> #include <dcmtk/dcmdata/dcvrfd.h> #include <dcmtk/dcmdata/dcvrfl.h> #include <dcmtk/dcmdata/dcvrsl.h> #include <dcmtk/dcmdata/dcvrss.h> #include <dcmtk/dcmdata/dcvrul.h> #include <dcmtk/dcmdata/dcvrus.h> +#include <dcmtk/dcmdata/dcuid.h> #include <boost/math/special_functions/round.hpp> 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(); @@ -604,4 +802,26 @@ result[GetName(it->first)] = it->second->AsString(); } } + + + std::string FromDcmtkBridge::GenerateUniqueIdentifier(DicomRootLevel level) + { + char uid[100]; + + switch (level) + { + case DicomRootLevel_Instance: + return dcmGenerateUniqueIdentifier(uid, SITE_INSTANCE_UID_ROOT); + + case DicomRootLevel_Series: + return dcmGenerateUniqueIdentifier(uid, SITE_SERIES_UID_ROOT); + + case DicomRootLevel_Study: + return dcmGenerateUniqueIdentifier(uid, SITE_STUDY_UID_ROOT); + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + }
--- a/OrthancServer/FromDcmtkBridge.h Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/FromDcmtkBridge.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -21,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 { @@ -33,6 +50,30 @@ ImageExtractionMode_UInt16 }; + enum DicomRootLevel + { + DicomRootLevel_Study, + DicomRootLevel_Series, + 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: @@ -93,5 +134,7 @@ static void ToJson(Json::Value& result, const DicomMap& values); + + static std::string GenerateUniqueIdentifier(DicomRootLevel level); }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/IServerIndexListener.h Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,52 @@ +/** + * 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 "ServerEnumerations.h" + +namespace Orthanc +{ + class IServerIndexListener + { + public: + virtual ~IServerIndexListener() + { + } + + virtual void SignalRemainingAncestor(ResourceType parentType, + const std::string& publicId) = 0; + + virtual void SignalFileDeleted(const FileInfo& info) = 0; + }; +}
--- a/OrthancServer/Internals/CommandDispatcher.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/Internals/CommandDispatcher.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/OrthancServer/Internals/CommandDispatcher.h Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/Internals/CommandDispatcher.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -23,7 +35,6 @@ #include "../DicomProtocol/DicomServer.h" #include "../../Core/MultiThreading/IRunnableBySteps.h" -#include <dcmtk/dcmnet/assoc.h> #include <dcmtk/dcmnet/dimse.h> namespace Orthanc
--- a/OrthancServer/Internals/FindScp.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/Internals/FindScp.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -24,11 +36,6 @@ #include "../ToDcmtkBridge.h" #include "../../Core/OrthancException.h" -#include <dcmtk/dcmdata/dcfilefo.h> -#include <dcmtk/dcmdata/dcmetinf.h> -#include <dcmtk/dcmdata/dcostrmb.h> -#include <dcmtk/dcmdata/dcdeftag.h> -#include <dcmtk/dcmnet/diutil.h> #include <glog/logging.h>
--- a/OrthancServer/Internals/FindScp.h Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/Internals/FindScp.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -22,7 +34,6 @@ #include "../DicomProtocol/IFindRequestHandler.h" -#include <dcmtk/dcmnet/assoc.h> #include <dcmtk/dcmnet/dimse.h> namespace Orthanc
--- a/OrthancServer/Internals/MoveScp.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/Internals/MoveScp.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -26,11 +38,6 @@ #include "../ToDcmtkBridge.h" #include "../../Core/OrthancException.h" -#include <dcmtk/dcmdata/dcfilefo.h> -#include <dcmtk/dcmdata/dcmetinf.h> -#include <dcmtk/dcmdata/dcostrmb.h> -#include <dcmtk/dcmdata/dcdeftag.h> -#include <dcmtk/dcmnet/diutil.h> #include <glog/logging.h>
--- a/OrthancServer/Internals/MoveScp.h Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/Internals/MoveScp.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -22,7 +34,6 @@ #include "../DicomProtocol/IMoveRequestHandler.h" -#include <dcmtk/dcmnet/assoc.h> #include <dcmtk/dcmnet/dimse.h> namespace Orthanc
--- a/OrthancServer/Internals/StoreScp.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/Internals/StoreScp.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -69,7 +81,7 @@ /*opt_groupLength*/ EGL_recalcGL, /*opt_paddingType*/ EPD_withoutPadding); #else - OFCondition c = dataSet->write(ob, xfer, encodingType); + OFCondition c = dataSet->write(ob, xfer, encodingType, NULL); #endif dataSet->transferEnd();
--- a/OrthancServer/Internals/StoreScp.h Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/Internals/StoreScp.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -22,7 +34,6 @@ #include "../DicomProtocol/IStoreRequestHandler.h" -#include <dcmtk/dcmnet/assoc.h> #include <dcmtk/dcmnet/dimse.h> namespace Orthanc
--- a/OrthancServer/OrthancInitialization.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/OrthancInitialization.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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
--- a/OrthancServer/OrthancInitialization.h Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/OrthancInitialization.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -23,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 Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/OrthancRestApi.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -20,157 +32,50 @@ #include "OrthancRestApi.h" -#include "OrthancInitialization.h" +#include "../Core/HttpServer/FilesystemHttpSender.h" +#include "../Core/Uuid.h" +#include "../Core/Compression/HierarchicalZipWriter.h" +#include "DicomProtocol/DicomUserConnection.h" #include "FromDcmtkBridge.h" -#include "../Core/Uuid.h" +#include "OrthancInitialization.h" +#include "ServerToolbox.h" #include <dcmtk/dcmdata/dcistrmb.h> #include <dcmtk/dcmdata/dcfilefo.h> #include <boost/lexical_cast.hpp> +#include <glog/logging.h> + + +#define RETRIEVE_CONTEXT(call) \ + OrthancRestApi& contextApi = \ + dynamic_cast<OrthancRestApi&>(call.GetContext()); \ + ServerContext& context = contextApi.GetContext() + +#define RETRIEVE_MODALITIES(call) \ + const OrthancRestApi::Modalities& modalities = \ + dynamic_cast<OrthancRestApi&>(call.GetContext()).GetModalities(); + + namespace Orthanc { - static void SendJson(HttpOutput& output, - const Json::Value& value) - { - Json::StyledWriter writer; - std::string s = writer.write(value); - output.AnswerBufferWithContentType(s, "application/json"); - } - - - static void SimplifyTagsRecursion(Json::Value& target, - const Json::Value& source) - { - assert(source.isObject()); - - target = Json::objectValue; - Json::Value::Members members = source.getMemberNames(); - - for (size_t i = 0; i < members.size(); i++) - { - const Json::Value& v = source[members[i]]; - const std::string& name = v["Name"].asString(); - const std::string& type = v["Type"].asString(); - - if (type == "String") - { - target[name] = v["Value"].asString(); - } - else if (type == "TooLong" || - type == "Null") - { - target[name] = Json::nullValue; - } - else if (type == "Sequence") - { - const Json::Value& array = v["Value"]; - assert(array.isArray()); - - Json::Value children = Json::arrayValue; - for (size_t i = 0; i < array.size(); i++) - { - Json::Value c; - SimplifyTagsRecursion(c, array[i]); - children.append(c); - } - - target[name] = children; - } - else - { - assert(0); - } - } - } - - - static void SimplifyTags(Json::Value& target, - const FileStorage& storage, - const std::string& fileUuid) - { - std::string s; - storage.ReadFile(s, fileUuid); + // DICOM SCU ---------------------------------------------------------------- - Json::Value source; - Json::Reader reader; - if (!reader.parse(s, source)) - { - throw OrthancException("Corrupted JSON file"); - } - - SimplifyTagsRecursion(target, source); - } - - - bool OrthancRestApi::Store(Json::Value& result, - const std::string& postData) - { - // Prepare an input stream for the memory buffer - DcmInputBufferStream is; - if (postData.size() > 0) - { - is.setBuffer(&postData[0], postData.size()); - } - is.setEos(); - - //printf("[%d]\n", postData.size()); - - DcmFileFormat dicomFile; - if (dicomFile.read(is).good()) - { - DicomMap dicomSummary; - FromDcmtkBridge::Convert(dicomSummary, *dicomFile.getDataset()); - - Json::Value dicomJson; - FromDcmtkBridge::ToJson(dicomJson, *dicomFile.getDataset()); - - std::string instanceUuid; - StoreStatus status = StoreStatus_Failure; - if (postData.size() > 0) - { - status = index_.Store - (instanceUuid, storage_, reinterpret_cast<const char*>(&postData[0]), - postData.size(), dicomSummary, dicomJson, ""); - } - - switch (status) - { - case StoreStatus_Success: - result["ID"] = instanceUuid; - result["Path"] = "/instances/" + instanceUuid; - result["Status"] = "Success"; - return true; - - case StoreStatus_AlreadyStored: - result["ID"] = instanceUuid; - result["Path"] = "/instances/" + instanceUuid; - result["Status"] = "AlreadyStored"; - return true; - - default: - return false; - } - } - - return false; - } - - void OrthancRestApi::ConnectToModality(DicomUserConnection& c, - const std::string& name) + static void ConnectToModality(DicomUserConnection& connection, + const std::string& name) { std::string aet, address; int port; GetDicomModality(name, aet, address, port); - c.SetLocalApplicationEntityTitle(GetGlobalStringParameter("DicomAet", "ORTHANC")); - c.SetDistantApplicationEntityTitle(aet); - c.SetDistantHost(address); - c.SetDistantPort(port); - c.Open(); + connection.SetLocalApplicationEntityTitle(GetGlobalStringParameter("DicomAet", "ORTHANC")); + connection.SetDistantApplicationEntityTitle(aet); + connection.SetDistantHost(address); + connection.SetDistantPort(port); + connection.Open(); } - bool OrthancRestApi::MergeQueryAndTemplate(DicomMap& result, - const std::string& postData) + static bool MergeQueryAndTemplate(DicomMap& result, + const std::string& postData) { Json::Value query; Json::Reader reader; @@ -191,100 +96,110 @@ return true; } - bool OrthancRestApi::DicomFindPatient(Json::Value& result, - DicomUserConnection& c, - const std::string& postData) + static void DicomFindPatient(RestApi::PostCall& call) { DicomMap m; DicomMap::SetupFindPatientTemplate(m); - if (!MergeQueryAndTemplate(m, postData)) + if (!MergeQueryAndTemplate(m, call.GetPostBody())) { - return false; + return; } + DicomUserConnection connection; + ConnectToModality(connection, call.GetUriComponent("id", "")); + DicomFindAnswers answers; - c.FindPatient(answers, m); + connection.FindPatient(answers, m); + + Json::Value result; answers.ToJson(result); - return true; + call.GetOutput().AnswerJson(result); } - bool OrthancRestApi::DicomFindStudy(Json::Value& result, - DicomUserConnection& c, - const std::string& postData) + static void DicomFindStudy(RestApi::PostCall& call) { DicomMap m; DicomMap::SetupFindStudyTemplate(m); - if (!MergeQueryAndTemplate(m, postData)) + if (!MergeQueryAndTemplate(m, call.GetPostBody())) { - return false; + return; } if (m.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 && m.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2) { - return false; + return; } - + + DicomUserConnection connection; + ConnectToModality(connection, call.GetUriComponent("id", "")); + DicomFindAnswers answers; - c.FindStudy(answers, m); + connection.FindStudy(answers, m); + + Json::Value result; answers.ToJson(result); - return true; + call.GetOutput().AnswerJson(result); } - bool OrthancRestApi::DicomFindSeries(Json::Value& result, - DicomUserConnection& c, - const std::string& postData) + static void DicomFindSeries(RestApi::PostCall& call) { DicomMap m; DicomMap::SetupFindSeriesTemplate(m); - if (!MergeQueryAndTemplate(m, postData)) + if (!MergeQueryAndTemplate(m, call.GetPostBody())) { - return false; + 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) { - return false; + return; } - + + DicomUserConnection connection; + ConnectToModality(connection, call.GetUriComponent("id", "")); + DicomFindAnswers answers; - c.FindSeries(answers, m); + connection.FindSeries(answers, m); + + Json::Value result; answers.ToJson(result); - return true; + call.GetOutput().AnswerJson(result); } - bool OrthancRestApi::DicomFind(Json::Value& result, - DicomUserConnection& c, - const std::string& postData) + static void DicomFind(RestApi::PostCall& call) { DicomMap m; DicomMap::SetupFindPatientTemplate(m); - if (!MergeQueryAndTemplate(m, postData)) + if (!MergeQueryAndTemplate(m, call.GetPostBody())) { - return false; + return; } - + + DicomUserConnection connection; + ConnectToModality(connection, call.GetUriComponent("id", "")); + DicomFindAnswers patients; - c.FindPatient(patients, m); + connection.FindPatient(patients, m); // Loop over the found patients - result = Json::arrayValue; + Json::Value result = Json::arrayValue; for (size_t i = 0; i < patients.GetSize(); i++) { Json::Value patient(Json::objectValue); FromDcmtkBridge::ToJson(patient, patients.GetAnswer(i)); DicomMap::SetupFindStudyTemplate(m); - if (!MergeQueryAndTemplate(m, postData)) + if (!MergeQueryAndTemplate(m, call.GetPostBody())) { - return false; + return; } m.CopyTagIfExists(patients.GetAnswer(i), DICOM_TAG_PATIENT_ID); DicomFindAnswers studies; - c.FindStudy(studies, m); + connection.FindStudy(studies, m); patient["Studies"] = Json::arrayValue; @@ -295,15 +210,15 @@ FromDcmtkBridge::ToJson(study, studies.GetAnswer(j)); DicomMap::SetupFindSeriesTemplate(m); - if (!MergeQueryAndTemplate(m, postData)) + if (!MergeQueryAndTemplate(m, call.GetPostBody())) { - return false; + return; } m.CopyTagIfExists(studies.GetAnswer(j), DICOM_TAG_PATIENT_ID); m.CopyTagIfExists(studies.GetAnswer(j), DICOM_TAG_STUDY_INSTANCE_UID); DicomFindAnswers series; - c.FindSeries(series, m); + connection.FindSeries(series, m); // Loop over the found series study["Series"] = Json::arrayValue; @@ -320,489 +235,690 @@ result.append(patient); } - return true; + call.GetOutput().AnswerJson(result); + } + + + static void DicomStore(RestApi::PostCall& call) + { + RETRIEVE_CONTEXT(call); + + std::string remote = call.GetUriComponent("id", ""); + DicomUserConnection connection; + ConnectToModality(connection, remote); + + const std::string& resourceId = call.GetPostBody(); + + Json::Value found; + if (context.GetIndex().LookupResource(found, resourceId, ResourceType_Series)) + { + // The UUID corresponds to a series + context.GetIndex().LogExportedResource(resourceId, remote); + + for (Json::Value::ArrayIndex i = 0; i < found["Instances"].size(); i++) + { + std::string instanceId = found["Instances"][i].asString(); + std::string dicom; + context.ReadFile(dicom, instanceId, FileContentType_Dicom); + connection.Store(dicom); + } + + call.GetOutput().AnswerBuffer("{}", "application/json"); + } + else if (context.GetIndex().LookupResource(found, resourceId, ResourceType_Instance)) + { + // The UUID corresponds to an instance + context.GetIndex().LogExportedResource(resourceId, remote); + + std::string dicom; + context.ReadFile(dicom, resourceId, FileContentType_Dicom); + connection.Store(dicom); + + call.GetOutput().AnswerBuffer("{}", "application/json"); + } + else + { + // The POST body is not a known resource, assume that it + // contains a raw DICOM instance + connection.Store(resourceId); + call.GetOutput().AnswerBuffer("{}", "application/json"); + } } - bool OrthancRestApi::DicomStore(Json::Value& result, - DicomUserConnection& c, - const std::string& postData) + // System information ------------------------------------------------------- + + static void ServeRoot(RestApi::GetCall& call) { - Json::Value found(Json::objectValue); + call.GetOutput().Redirect("app/explorer.html"); + } + + static void GetSystemInformation(RestApi::GetCall& call) + { + Json::Value result = Json::objectValue; + + result["Version"] = ORTHANC_VERSION; + result["Name"] = GetGlobalStringParameter("Name", ""); + + call.GetOutput().AnswerJson(result); + } - if (!Toolbox::IsUuid(postData)) - { - // This is not a UUID, assume this is a DICOM instance - c.Store(postData); - } - else if (index_.GetSeries(found, postData)) + static void GetStatistics(RestApi::GetCall& call) + { + RETRIEVE_CONTEXT(call); + Json::Value result = Json::objectValue; + context.GetIndex().ComputeStatistics(result); + call.GetOutput().AnswerJson(result); + } + + + // List all the patients, studies, series or instances ---------------------- + + template <enum ResourceType resourceType> + static void ListResources(RestApi::GetCall& call) + { + RETRIEVE_CONTEXT(call); + + Json::Value result; + context.GetIndex().GetAllUuids(result, resourceType); + call.GetOutput().AnswerJson(result); + } + + template <enum ResourceType resourceType> + static void GetSingleResource(RestApi::GetCall& call) + { + RETRIEVE_CONTEXT(call); + + Json::Value result; + if (context.GetIndex().LookupResource(result, call.GetUriComponent("id", ""), resourceType)) { - // The UUID corresponds to a series - for (size_t i = 0; i < found["Instances"].size(); i++) + call.GetOutput().AnswerJson(result); + } + } + + template <enum ResourceType resourceType> + static void DeleteSingleResource(RestApi::DeleteCall& call) + { + RETRIEVE_CONTEXT(call); + + Json::Value result; + if (context.GetIndex().DeleteResource(result, call.GetUriComponent("id", ""), resourceType)) + { + call.GetOutput().AnswerJson(result); + } + } + + + // Download of ZIP files ---------------------------------------------------- + + + static std::string GetDirectoryNameInArchive(const Json::Value& resource, + ResourceType resourceType) + { + switch (resourceType) + { + case ResourceType_Patient: + { + std::string p = resource["MainDicomTags"]["PatientID"].asString(); + std::string n = resource["MainDicomTags"]["PatientName"].asString(); + return p + " " + n; + } + + case ResourceType_Study: { - std::string uuid = found["Instances"][i].asString(); - Json::Value instance(Json::objectValue); - if (index_.GetInstance(instance, uuid)) - { - std::string content; - storage_.ReadFile(content, instance["FileUuid"].asString()); - c.Store(content); - } - else + return resource["MainDicomTags"]["StudyDescription"].asString(); + } + + case ResourceType_Series: + { + std::string d = resource["MainDicomTags"]["SeriesDescription"].asString(); + std::string m = resource["MainDicomTags"]["Modality"].asString(); + return m + " " + d; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + static bool CreateRootDirectoryInArchive(HierarchicalZipWriter& writer, + ServerContext& context, + const Json::Value& resource, + ResourceType resourceType) + { + if (resourceType == ResourceType_Patient) + { + return true; + } + + ResourceType parentType = GetParentResourceType(resourceType); + Json::Value parent; + + switch (resourceType) + { + case ResourceType_Study: + { + if (!context.GetIndex().LookupResource(parent, resource["ParentPatient"].asString(), parentType)) { return false; } + + break; } + + case ResourceType_Series: + if (!context.GetIndex().LookupResource(parent, resource["ParentStudy"].asString(), parentType) || + !CreateRootDirectoryInArchive(writer, context, parent, parentType)) + { + return false; + } + break; + + default: + throw OrthancException(ErrorCode_NotImplemented); } - else if (index_.GetInstance(found, postData)) - { - // The UUID corresponds to an instance - std::string content; - storage_.ReadFile(content, found["FileUuid"].asString()); - c.Store(content); - } - else + + writer.OpenDirectory(GetDirectoryNameInArchive(parent, parentType).c_str()); + return true; + } + + static bool ArchiveInstance(HierarchicalZipWriter& writer, + ServerContext& context, + const std::string& instancePublicId) + { + Json::Value instance; + if (!context.GetIndex().LookupResource(instance, instancePublicId, ResourceType_Instance)) { return false; } + std::string filename = instance["MainDicomTags"]["SOPInstanceUID"].asString() + ".dcm"; + writer.OpenFile(filename.c_str()); + + std::string dicom; + context.ReadFile(dicom, instancePublicId, FileContentType_Dicom); + writer.Write(dicom); + return true; } - - OrthancRestApi::OrthancRestApi(ServerIndex& index, - const std::string& path) : - index_(index), - storage_(path) + static bool ArchiveInternal(HierarchicalZipWriter& writer, + ServerContext& context, + const std::string& publicId, + ResourceType resourceType, + bool isFirstLevel) { - GetListOfDicomModalities(modalities_); - } - - - void OrthancRestApi::Handle( - HttpOutput& output, - const std::string& method, - const UriComponents& uri, - const Arguments& headers, - const Arguments& arguments, - const std::string& postData) - { - if (uri.size() == 0) + Json::Value resource; + if (!context.GetIndex().LookupResource(resource, publicId, resourceType)) { - if (method == "GET") - { - output.Redirect("app/explorer.html"); - } - else - { - output.SendMethodNotAllowedError("GET"); - } - - return; + return false; } - bool existingResource = false; - Json::Value result(Json::objectValue); - - - // List all the instances --------------------------------------------------- - - if (uri.size() == 1 && uri[0] == "instances") + if (isFirstLevel && + !CreateRootDirectoryInArchive(writer, context, resource, resourceType)) { - if (method == "GET") - { - result = Json::Value(Json::arrayValue); - index_.GetAllUuids(result, "Instances"); - existingResource = true; - } - else if (method == "POST") - { - // Add a new instance to the storage - if (Store(result, postData)) - { - SendJson(output, result); - return; - } - else - { - output.SendHeader(Orthanc_HttpStatus_415_UnsupportedMediaType); - return; - } - } - else - { - output.SendMethodNotAllowedError("GET,POST"); - return; - } + return false; } + writer.OpenDirectory(GetDirectoryNameInArchive(resource, resourceType).c_str()); - // List all the patients, studies or series --------------------------------- - - if (uri.size() == 1 && - (uri[0] == "series" || - uri[0] == "studies" || - uri[0] == "patients")) + switch (resourceType) { - if (method == "GET") - { - result = Json::Value(Json::arrayValue); + case ResourceType_Patient: + for (size_t i = 0; i < resource["Studies"].size(); i++) + { + std::string studyId = resource["Studies"][i].asString(); + if (!ArchiveInternal(writer, context, studyId, ResourceType_Study, false)) + { + return false; + } + } + break; - if (uri[0] == "instances") - index_.GetAllUuids(result, "Instances"); - else if (uri[0] == "series") - index_.GetAllUuids(result, "Series"); - else if (uri[0] == "studies") - index_.GetAllUuids(result, "Studies"); - else if (uri[0] == "patients") - index_.GetAllUuids(result, "Patients"); + case ResourceType_Study: + for (size_t i = 0; i < resource["Series"].size(); i++) + { + std::string seriesId = resource["Series"][i].asString(); + if (!ArchiveInternal(writer, context, seriesId, ResourceType_Series, false)) + { + return false; + } + } + break; - existingResource = true; - } - else - { - output.SendMethodNotAllowedError("GET"); - return; - } + case ResourceType_Series: + for (size_t i = 0; i < resource["Instances"].size(); i++) + { + if (!ArchiveInstance(writer, context, resource["Instances"][i].asString())) + { + return false; + } + } + break; + + default: + throw OrthancException(ErrorCode_InternalError); } + writer.CloseDirectory(); + return true; + } - // Information about a single object ---------------------------------------- - - else if (uri.size() == 2 && - (uri[0] == "instances" || - uri[0] == "series" || - uri[0] == "studies" || - uri[0] == "patients")) + template <enum ResourceType resourceType> + static void GetArchive(RestApi::GetCall& call) + { + RETRIEVE_CONTEXT(call); + + // Create a RAII for the temporary file to manage the ZIP file + Toolbox::TemporaryFile tmp; + std::string id = call.GetUriComponent("id", ""); + { - if (method == "GET") + // Create a ZIP writer + HierarchicalZipWriter writer(tmp.GetPath().c_str()); + + // Store the requested resource into the ZIP + if (!ArchiveInternal(writer, context, id, resourceType, true)) { - if (uri[0] == "patients") - { - existingResource = index_.GetPatient(result, uri[1]); - } - else if (uri[0] == "studies") - { - existingResource = index_.GetStudy(result, uri[1]); - } - else if (uri[0] == "series") - { - existingResource = index_.GetSeries(result, uri[1]); - } - else if (uri[0] == "instances") - { - existingResource = index_.GetInstance(result, uri[1]); - } - } - else if (method == "DELETE") - { - if (uri[0] == "patients") - { - existingResource = index_.DeletePatient(result, uri[1]); - } - else if (uri[0] == "studies") - { - existingResource = index_.DeleteStudy(result, uri[1]); - } - else if (uri[0] == "series") - { - existingResource = index_.DeleteSeries(result, uri[1]); - } - else if (uri[0] == "instances") - { - existingResource = index_.DeleteInstance(result, uri[1]); - } - - if (existingResource) - { - result["Status"] = "Success"; - } - } - else - { - output.SendMethodNotAllowedError("GET,DELETE"); return; } } + // Prepare the sending of the ZIP file + FilesystemHttpSender sender(tmp.GetPath().c_str()); + sender.SetContentType("application/zip"); + sender.SetDownloadFilename(id + ".zip"); - // Get the DICOM or the JSON file of one instance --------------------------- + // Send the ZIP + call.GetOutput().AnswerFile(sender); + + // The temporary file is automatically removed thanks to the RAII + } + + + // Changes API -------------------------------------------------------------- - else if (uri.size() == 3 && - uri[0] == "instances" && - (uri[2] == "file" || - uri[2] == "tags" || - uri[2] == "simplified-tags")) + static void GetSinceAndLimit(int64_t& since, + unsigned int& limit, + bool& last, + const RestApi::GetCall& call) + { + static const unsigned int MAX_RESULTS = 100; + + if (call.HasArgument("last")) { - std::string fileUuid, contentType; - if (uri[2] == "file") - { - existingResource = index_.GetDicomFile(fileUuid, uri[1]); - contentType = "application/dicom"; - } - else if (uri[2] == "tags" || - uri[2] == "simplified-tags") - { - existingResource = index_.GetJsonFile(fileUuid, uri[1]); - contentType = "application/json"; - } + last = true; + return; + } + + last = false; - if (existingResource) - { - if (uri[2] == "simplified-tags") - { - Json::Value v; - SimplifyTags(v, storage_, fileUuid); - SendJson(output, v); - return; - } - else - { - output.AnswerFile(storage_, fileUuid, contentType); - return; - } - } + try + { + since = boost::lexical_cast<int64_t>(call.GetArgument("since", "0")); + limit = boost::lexical_cast<unsigned int>(call.GetArgument("limit", "0")); + } + catch (boost::bad_lexical_cast) + { + return; } + if (limit == 0 || limit > MAX_RESULTS) + { + limit = MAX_RESULTS; + } + } - else if (uri.size() == 3 && - uri[0] == "instances" && - uri[2] == "frames") + static void GetChanges(RestApi::GetCall& call) + { + RETRIEVE_CONTEXT(call); + + //std::string filter = GetArgument(getArguments, "filter", ""); + int64_t since; + unsigned int limit; + bool last; + GetSinceAndLimit(since, limit, last, call); + + Json::Value result; + if ((!last && context.GetIndex().GetChanges(result, since, limit)) || + ( last && context.GetIndex().GetLastChange(result))) { - Json::Value instance(Json::objectValue); - existingResource = index_.GetInstance(instance, uri[1]); + call.GetOutput().AnswerJson(result); + } + } + + + static void GetExports(RestApi::GetCall& call) + { + RETRIEVE_CONTEXT(call); - if (existingResource) - { - result = Json::arrayValue; + int64_t since; + unsigned int limit; + bool last; + GetSinceAndLimit(since, limit, last, call); + + Json::Value result; + if ((!last && context.GetIndex().GetExportedResources(result, since, limit)) || + ( last && context.GetIndex().GetLastExportedResource(result))) + { + call.GetOutput().AnswerJson(result); + } + } - unsigned int numberOfFrames = 1; - try - { - Json::Value tmp = instance["MainDicomTags"]["NumberOfFrames"]; - numberOfFrames = boost::lexical_cast<unsigned int>(tmp.asString()); - } - catch (boost::bad_lexical_cast) - { - } + + // 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()); - for (unsigned int i = 0; i < numberOfFrames; i++) - { - result.append(i); - } - } + 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) + { + RETRIEVE_CONTEXT(call); + + std::string publicId = call.GetUriComponent("id", ""); + context.AnswerFile(call.GetOutput(), publicId, FileContentType_Dicom); + } - else if (uri[0] == "instances" && - ((uri.size() == 3 && - (uri[2] == "preview" || - uri[2] == "image-uint8" || - uri[2] == "image-uint16")) || - (uri.size() == 5 && - uri[2] == "frames" && - (uri[4] == "preview" || - uri[4] == "image-uint8" || - uri[4] == "image-uint16")))) + template <bool simplify> + static void GetInstanceTags(RestApi::GetCall& call) + { + RETRIEVE_CONTEXT(call); + + std::string publicId = call.GetUriComponent("id", ""); + + Json::Value full; + context.ReadJson(full, publicId); + + if (simplify) { - std::string uuid; - existingResource = index_.GetDicomFile(uuid, uri[1]); + Json::Value simplified; + SimplifyTags(simplified, full); + call.GetOutput().AnswerJson(simplified); + } + else + { + call.GetOutput().AnswerJson(full); + } + } - std::string action = uri[2]; + + static void ListFrames(RestApi::GetCall& call) + { + RETRIEVE_CONTEXT(call); - unsigned int frame = 0; - if (existingResource && - uri.size() == 5) + Json::Value instance; + if (context.GetIndex().LookupResource(instance, call.GetUriComponent("id", ""), ResourceType_Instance)) + { + unsigned int numberOfFrames = 1; + + try { - // Access to multi-frame image - action = uri[4]; - try - { - frame = boost::lexical_cast<unsigned int>(uri[3]); - } - catch (boost::bad_lexical_cast) - { - existingResource = false; - } + Json::Value tmp = instance["MainDicomTags"]["NumberOfFrames"]; + numberOfFrames = boost::lexical_cast<unsigned int>(tmp.asString()); + } + catch (...) + { + } + + Json::Value result = Json::arrayValue; + for (unsigned int i = 0; i < numberOfFrames; i++) + { + result.append(i); } - if (existingResource) + call.GetOutput().AnswerJson(result); + } + } + + + template <enum ImageExtractionMode mode> + static void GetImage(RestApi::GetCall& call) + { + RETRIEVE_CONTEXT(call); + + std::string frameId = call.GetUriComponent("frame", "0"); + + unsigned int frame; + try + { + frame = boost::lexical_cast<unsigned int>(frameId); + } + catch (boost::bad_lexical_cast) + { + return; + } + + std::string publicId = call.GetUriComponent("id", ""); + std::string dicomContent, png; + context.ReadFile(dicomContent, publicId, FileContentType_Dicom); + + try + { + FromDcmtkBridge::ExtractPngImage(png, dicomContent, frame, mode); + call.GetOutput().AnswerBuffer(png, "image/png"); + } + catch (OrthancException& e) + { + if (e.GetErrorCode() == ErrorCode_ParameterOutOfRange) { - std::string dicomContent, png; - storage_.ReadFile(dicomContent, uuid); - try + // The frame number is out of the range for this DICOM + // instance, the resource is not existent + } + else + { + std::string root = ""; + for (size_t i = 1; i < call.GetFullUri().size(); i++) { - if (action == "preview") - { - FromDcmtkBridge::ExtractPngImage(png, dicomContent, frame, ImageExtractionMode_Preview); - } - else if (action == "image-uint8") - { - FromDcmtkBridge::ExtractPngImage(png, dicomContent, frame, ImageExtractionMode_UInt8); - } - else if (action == "image-uint16") - { - FromDcmtkBridge::ExtractPngImage(png, dicomContent, frame, ImageExtractionMode_UInt16); - } - else - { - throw OrthancException(ErrorCode_InternalError); - } + root += "../"; + } - output.AnswerBufferWithContentType(png, "image/png"); - return; - } - catch (OrthancException&) - { - std::string root = ""; - for (size_t i = 1; i < uri.size(); i++) - { - root += "../"; - } - - output.Redirect(root + "app/images/Unsupported.png"); - return; - } + call.GetOutput().Redirect(root + "app/images/unsupported.png"); } } + } + + + // Upload of DICOM files through HTTP --------------------------------------- + + static void UploadDicomFile(RestApi::PostCall& call) + { + RETRIEVE_CONTEXT(call); + + const std::string& postData = call.GetPostBody(); + + LOG(INFO) << "Receiving a DICOM file of " << postData.size() << " bytes through HTTP"; + + // Prepare an input stream for the memory buffer + DcmInputBufferStream is; + if (postData.size() > 0) + { + is.setBuffer(&postData[0], postData.size()); + } + is.setEos(); + + DcmFileFormat dicomFile; + if (!dicomFile.read(is).good()) + { + call.GetOutput().SignalError(Orthanc_HttpStatus_415_UnsupportedMediaType); + return; + } + + DicomMap dicomSummary; + FromDcmtkBridge::Convert(dicomSummary, *dicomFile.getDataset()); + + DicomInstanceHasher hasher(dicomSummary); + + Json::Value dicomJson; + FromDcmtkBridge::ToJson(dicomJson, *dicomFile.getDataset()); + + StoreStatus status = StoreStatus_Failure; + if (postData.size() > 0) + { + status = context.Store + (reinterpret_cast<const char*>(&postData[0]), + postData.size(), dicomSummary, dicomJson, ""); + } + + Json::Value result = Json::objectValue; + + if (status != StoreStatus_Failure) + { + result["ID"] = hasher.HashInstance(); + result["Path"] = GetBasePath(ResourceType_Instance, hasher.HashInstance()); + } + + result["Status"] = ToString(status); + call.GetOutput().AnswerJson(result); + } + + + // DICOM bridge ------------------------------------------------------------- + + static bool IsExistingModality(const OrthancRestApi::Modalities& modalities, + const std::string& id) + { + return modalities.find(id) != modalities.end(); + } + + static void ListModalities(RestApi::GetCall& call) + { + RETRIEVE_MODALITIES(call); + + Json::Value result = Json::arrayValue; + for (OrthancRestApi::Modalities::const_iterator + it = modalities.begin(); it != modalities.end(); it++) + { + result.append(*it); + } + + call.GetOutput().AnswerJson(result); + } + + + static void ListModalityOperations(RestApi::GetCall& call) + { + RETRIEVE_MODALITIES(call); + + std::string id = call.GetUriComponent("id", ""); + if (IsExistingModality(modalities, id)) + { + Json::Value result = Json::arrayValue; + result.append("find-patient"); + result.append("find-study"); + result.append("find-series"); + result.append("find"); + result.append("store"); + call.GetOutput().AnswerJson(result); + } + } - // Changes API -------------------------------------------------------------- - - if (uri.size() == 1 && uri[0] == "changes") - { - if (method == "GET") - { - const static unsigned int MAX_RESULTS = 100; + // 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_); - std::string filter = GetArgument(arguments, "filter", ""); - int64_t since; - unsigned int limit; - try - { - since = boost::lexical_cast<int64_t>(GetArgument(arguments, "since", "0")); - limit = boost::lexical_cast<unsigned int>(GetArgument(arguments, "limit", "0")); - } - catch (boost::bad_lexical_cast) - { - output.SendHeader(Orthanc_HttpStatus_400_BadRequest); - return; - } + RETRIEVE_CONTEXT(call); + std::string id = call.GetUriComponent("id", ""); + ParsedDicomFile& dicom = context.GetDicomFile(id); + dicom.SendPathValue(call.GetOutput(), call.GetTrailingUri()); + } - if (limit == 0 || limit > MAX_RESULTS) - { - limit = MAX_RESULTS; - } - if (!index_.GetChanges(result, since, filter, limit)) - { - output.SendHeader(Orthanc_HttpStatus_400_BadRequest); - return; - } - - existingResource = true; - } - else - { - output.SendMethodNotAllowedError("GET"); - return; - } - } - // DICOM bridge ------------------------------------------------------------- + // Registration of the various REST handlers -------------------------------- - if (uri.size() == 1 && - uri[0] == "modalities") - { - if (method == "GET") - { - result = Json::Value(Json::arrayValue); - existingResource = true; + OrthancRestApi::OrthancRestApi(ServerContext& context) : + context_(context) + { + GetListOfDicomModalities(modalities_); + + Register("/", ServeRoot); + Register("/system", GetSystemInformation); + Register("/statistics", GetStatistics); + Register("/changes", GetChanges); + Register("/exports", GetExports); - for (Modalities::const_iterator it = modalities_.begin(); - it != modalities_.end(); it++) - { - result.append(*it); - } - } - else - { - output.SendMethodNotAllowedError("GET"); - return; - } - } + Register("/instances", UploadDicomFile); + Register("/instances", ListResources<ResourceType_Instance>); + Register("/patients", ListResources<ResourceType_Patient>); + Register("/series", ListResources<ResourceType_Series>); + Register("/studies", ListResources<ResourceType_Study>); + + Register("/instances/{id}", DeleteSingleResource<ResourceType_Instance>); + Register("/instances/{id}", GetSingleResource<ResourceType_Instance>); + Register("/patients/{id}", DeleteSingleResource<ResourceType_Patient>); + Register("/patients/{id}", GetSingleResource<ResourceType_Patient>); + Register("/series/{id}", DeleteSingleResource<ResourceType_Series>); + Register("/series/{id}", GetSingleResource<ResourceType_Series>); + Register("/studies/{id}", DeleteSingleResource<ResourceType_Study>); + Register("/studies/{id}", GetSingleResource<ResourceType_Study>); - if ((uri.size() == 2 || - uri.size() == 3) && - uri[0] == "modalities") - { - if (modalities_.find(uri[1]) == modalities_.end()) - { - // Unknown modality - } - else if (uri.size() == 2) - { - if (method != "GET") - { - output.SendMethodNotAllowedError("POST"); - return; - } - else - { - existingResource = true; - result = Json::arrayValue; - result.append("find-patient"); - result.append("find-study"); - result.append("find-series"); - result.append("find"); - result.append("store"); - } - } - else if (uri.size() == 3) - { - if (uri[2] != "find-patient" && - uri[2] != "find-study" && - uri[2] != "find-series" && - uri[2] != "find" && - uri[2] != "store") - { - // Unknown request - } - else if (method != "POST") - { - output.SendMethodNotAllowedError("POST"); - return; - } - else - { - DicomUserConnection connection; - ConnectToModality(connection, uri[1]); - existingResource = true; - - if ((uri[2] == "find-patient" && !DicomFindPatient(result, connection, postData)) || - (uri[2] == "find-study" && !DicomFindStudy(result, connection, postData)) || - (uri[2] == "find-series" && !DicomFindSeries(result, connection, postData)) || - (uri[2] == "find" && !DicomFind(result, connection, postData)) || - (uri[2] == "store" && !DicomStore(result, connection, postData))) - { - output.SendHeader(Orthanc_HttpStatus_400_BadRequest); - return; - } - } - } - } + Register("/patients/{id}/archive", GetArchive<ResourceType_Patient>); + 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); - - if (existingResource) - { - SendJson(output, result); - } - else - { - output.SendHeader(Orthanc_HttpStatus_404_NotFound); - } + Register("/instances/{id}/frames/{frame}/preview", GetImage<ImageExtractionMode_Preview>); + Register("/instances/{id}/frames/{frame}/image-uint8", GetImage<ImageExtractionMode_UInt8>); + Register("/instances/{id}/frames/{frame}/image-uint16", GetImage<ImageExtractionMode_UInt16>); + Register("/instances/{id}/preview", GetImage<ImageExtractionMode_Preview>); + Register("/instances/{id}/image-uint8", GetImage<ImageExtractionMode_UInt8>); + Register("/instances/{id}/image-uint16", GetImage<ImageExtractionMode_UInt16>); + + Register("/modalities", ListModalities); + Register("/modalities/{id}", ListModalityOperations); + Register("/modalities/{id}/find-patient", DicomFindPatient); + Register("/modalities/{id}/find-study", DicomFindStudy); + Register("/modalities/{id}/find-series", DicomFindSeries); + Register("/modalities/{id}/find", DicomFind); + Register("/modalities/{id}/store", DicomStore); + + // TODO : "content" } }
--- a/OrthancServer/OrthancRestApi.h Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/OrthancRestApi.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -20,68 +32,33 @@ #pragma once -#include "../Core/HttpServer/HttpHandler.h" -#include "ServerIndex.h" -#include "DicomProtocol/DicomUserConnection.h" +#include "ServerContext.h" +#include "../Core/RestApi/RestApi.h" #include <set> - namespace Orthanc { - class OrthancRestApi : public HttpHandler + class OrthancRestApi : public RestApi { - private: + public: typedef std::set<std::string> Modalities; - ServerIndex& index_; - FileStorage storage_; + private: + ServerContext& context_; Modalities modalities_; - bool Store(Json::Value& result, - const std::string& postData); - - void ConnectToModality(DicomUserConnection& c, - const std::string& name); - - bool MergeQueryAndTemplate(DicomMap& result, - const std::string& postData); - - bool DicomFindPatient(Json::Value& result, - DicomUserConnection& c, - const std::string& postData); - - bool DicomFindStudy(Json::Value& result, - DicomUserConnection& c, - const std::string& postData); + public: + OrthancRestApi(ServerContext& context); - bool DicomFindSeries(Json::Value& result, - DicomUserConnection& c, - const std::string& postData); - - bool DicomFind(Json::Value& result, - DicomUserConnection& c, - const std::string& postData); - - bool DicomStore(Json::Value& result, - DicomUserConnection& c, - const std::string& postData); - - public: - OrthancRestApi(ServerIndex& index, - const std::string& path); - - virtual bool IsServedUri(const UriComponents& uri) + ServerContext& GetContext() { - return true; + return context_; } - virtual void Handle( - HttpOutput& output, - const std::string& method, - const UriComponents& uri, - const Arguments& headers, - const Arguments& arguments, - const std::string& postData); + Modalities& GetModalities() + { + return modalities_; + } }; }
--- a/OrthancServer/PrepareDatabase.sql Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/PrepareDatabase.sql Fri Dec 14 11:24:24 2012 +0100 @@ -1,136 +1,106 @@ CREATE TABLE GlobalProperties( - name TEXT PRIMARY KEY, + property INTEGER PRIMARY KEY, value TEXT ); CREATE TABLE Resources( - uuid TEXT PRIMARY KEY, - resourceType INTEGER - ); - -CREATE TABLE Patients( - uuid TEXT PRIMARY KEY, - dicomPatientId TEXT - ); - -CREATE TABLE Studies( - uuid TEXT PRIMARY KEY, - parentPatient TEXT REFERENCES Patients(uuid) ON DELETE CASCADE, - dicomStudy TEXT - ); - -CREATE TABLE Series( - uuid TEXT PRIMARY KEY, - parentStudy TEXT REFERENCES Studies(uuid) ON DELETE CASCADE, - dicomSeries TEXT, - expectedNumberOfInstances INTEGER - ); - -CREATE TABLE Instances( - uuid TEXT PRIMARY KEY, - parentSeries TEXT REFERENCES Series(uuid) ON DELETE CASCADE, - dicomInstance TEXT, - fileUuid TEXT, - fileSize INTEGER, - jsonUuid TEXT, - distantAet TEXT, - indexInSeries INTEGER + internalId INTEGER PRIMARY KEY AUTOINCREMENT, + resourceType INTEGER, + publicId TEXT, + parentId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE ); CREATE TABLE MainDicomTags( - uuid TEXT, + id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, tagGroup INTEGER, tagElement INTEGER, value TEXT, - PRIMARY KEY(uuid, tagGroup, tagElement) + PRIMARY KEY(id, tagGroup, tagElement) + ); + +CREATE TABLE Metadata( + id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, + type INTEGER, + value TEXT, + PRIMARY KEY(id, type) ); +CREATE TABLE AttachedFiles( + id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, + fileType INTEGER, + uuid TEXT, + compressedSize INTEGER, + uncompressedSize INTEGER, + compressionType INTEGER, + PRIMARY KEY(id, fileType) + ); + CREATE TABLE Changes( seq INTEGER PRIMARY KEY AUTOINCREMENT, - basePath TEXT, - uuid TEXT + changeType INTEGER, + internalId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, + resourceType INTEGER, + date TEXT + ); + +CREATE TABLE ExportedResources( + seq INTEGER PRIMARY KEY AUTOINCREMENT, + resourceType INTEGER, + publicId TEXT, + remoteModality TEXT, + patientId TEXT, + studyInstanceUid TEXT, + seriesInstanceUid TEXT, + sopInstanceUid TEXT, + date TEXT + ); + +CREATE TABLE PatientRecyclingOrder( + seq INTEGER PRIMARY KEY AUTOINCREMENT, + patientId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE ); - -CREATE INDEX PatientToStudies ON Studies(parentPatient); -CREATE INDEX StudyToSeries ON Series(parentStudy); -CREATE INDEX SeriesToInstances ON Instances(parentSeries); - -CREATE INDEX DicomPatientIndex ON Patients(dicomPatientId); -CREATE INDEX DicomStudyIndex ON Studies(dicomStudy); -CREATE INDEX DicomSeriesIndex ON Series(dicomSeries); -CREATE INDEX DicomInstanceIndex ON Instances(dicomInstance); +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 MainDicomTagsIndex ON MainDicomTags(uuid); -CREATE INDEX MainDicomTagsGroupElement ON MainDicomTags(tagGroup, tagElement); -CREATE INDEX MainDicomTagsValues ON MainDicomTags(value COLLATE BINARY); +CREATE INDEX MainDicomTagsIndex1 ON MainDicomTags(id); +CREATE INDEX MainDicomTagsIndex2 ON MainDicomTags(tagGroup, tagElement); +CREATE INDEX MainDicomTagsIndexValues ON MainDicomTags(value COLLATE BINARY); -CREATE INDEX ChangesIndex ON Changes(uuid); +CREATE INDEX ChangesIndex ON Changes(internalId); -CREATE TRIGGER InstanceRemoved -AFTER DELETE ON Instances -FOR EACH ROW BEGIN - DELETE FROM Resources WHERE uuid = old.uuid; - DELETE FROM MainDicomTags WHERE uuid = old.uuid; - DELETE FROM Changes WHERE uuid = old.uuid; - SELECT DeleteFromFileStorage(old.fileUuid); - SELECT DeleteFromFileStorage(old.jsonUuid); - SELECT SignalDeletedLevel(3, old.parentSeries); +CREATE TRIGGER AttachedFileDeleted +AFTER DELETE ON AttachedFiles +BEGIN + SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, + old.compressionType, old.compressedSize); END; -CREATE TRIGGER SeriesRemoved -AFTER DELETE ON Series -FOR EACH ROW BEGIN - DELETE FROM Resources WHERE uuid = old.uuid; - DELETE FROM MainDicomTags WHERE uuid = old.uuid; - DELETE FROM Changes WHERE uuid = old.uuid; - SELECT SignalDeletedLevel(2, old.parentStudy); +CREATE TRIGGER ResourceDeleted +AFTER DELETE ON Resources +BEGIN + SELECT SignalRemainingAncestor(parent.publicId, parent.resourceType) + FROM Resources AS parent WHERE internalId = old.parentId; END; -CREATE TRIGGER StudyRemoved -AFTER DELETE ON Studies -FOR EACH ROW BEGIN - DELETE FROM Resources WHERE uuid = old.uuid; - DELETE FROM MainDicomTags WHERE uuid = old.uuid; - DELETE FROM Changes WHERE uuid = old.uuid; - SELECT SignalDeletedLevel(1, old.parentPatient); +-- Delete a parent resource when its unique child is deleted +CREATE TRIGGER ResourceDeletedParentCleaning +AFTER DELETE ON Resources +FOR EACH ROW WHEN (SELECT COUNT(*) FROM Resources WHERE parentId = old.parentId) = 0 +BEGIN + DELETE FROM Resources WHERE internalId = old.parentId; END; -CREATE TRIGGER PatientRemoved -AFTER DELETE ON Patients -FOR EACH ROW BEGIN - DELETE FROM Resources WHERE uuid = old.uuid; - DELETE FROM MainDicomTags WHERE uuid = old.uuid; - DELETE FROM Changes WHERE uuid = old.uuid; - SELECT SignalDeletedLevel(0, ""); +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; - - -CREATE TRIGGER InstanceRemovedUpwardCleaning -AFTER DELETE ON Instances -FOR EACH ROW - WHEN (SELECT COUNT(*) FROM Instances WHERE parentSeries = old.parentSeries) = 0 - BEGIN - SELECT DeleteFromFileStorage("deleting parent series"); -- TODO REMOVE THIS - DELETE FROM Series WHERE uuid = old.parentSeries; - END; - -CREATE TRIGGER SeriesRemovedUpwardCleaning -AFTER DELETE ON Series -FOR EACH ROW - WHEN (SELECT COUNT(*) FROM Series WHERE parentStudy = old.parentStudy) = 0 - BEGIN - SELECT DeleteFromFileStorage("deleting parent study"); -- TODO REMOVE THIS - DELETE FROM Studies WHERE uuid = old.parentStudy; - END; - -CREATE TRIGGER StudyRemovedUpwardCleaning -AFTER DELETE ON Studies -FOR EACH ROW - WHEN (SELECT COUNT(*) FROM Studies WHERE parentPatient = old.parentPatient) = 0 - BEGIN - SELECT DeleteFromFileStorage("deleting parent patient"); -- TODO REMOVE THIS - DELETE FROM Patients WHERE uuid = old.parentPatient; - END; +-- Set the version of the database schema +-- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration +INSERT INTO GlobalProperties VALUES (1, "3");
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerContext.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,183 @@ +/** + * 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 "ServerContext.h" + +#include "../Core/HttpServer/FilesystemHttpSender.h" + +#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 + * since the filesystem implements the required locking mechanisms, + * but maybe a read-writer lock on the "FileStorage" could be + * useful. Conversely, "ServerIndex" already implements mutex-based + * locking. + **/ + +namespace Orthanc +{ + ServerContext::ServerContext(const boost::filesystem::path& path) : + storage_(path.string()), + index_(*this, path.string()), + accessor_(storage_), + provider_(*this), + dicomCache_(provider_, DICOM_CACHE_SIZE) + { + } + + void ServerContext::SetCompressionEnabled(bool enabled) + { + if (enabled) + LOG(WARNING) << "Disk compression is enabled"; + else + LOG(WARNING) << "Disk compression is disabled"; + + compressionEnabled_ = enabled; + } + + void ServerContext::RemoveFile(const std::string& fileUuid) + { + storage_.Remove(fileUuid); + } + + StoreStatus ServerContext::Store(const char* dicomFile, + size_t dicomSize, + const DicomMap& dicomSummary, + const Json::Value& dicomJson, + const std::string& remoteAet) + { + if (compressionEnabled_) + { + accessor_.SetCompressionForNextOperations(CompressionType_Zlib); + } + else + { + accessor_.SetCompressionForNextOperations(CompressionType_None); + } + + FileInfo dicomInfo = accessor_.Write(dicomFile, dicomSize, FileContentType_Dicom); + FileInfo jsonInfo = accessor_.Write(dicomJson.toStyledString(), FileContentType_Json); + + ServerIndex::Attachments attachments; + attachments.push_back(dicomInfo); + attachments.push_back(jsonInfo); + + StoreStatus status = index_.Store(dicomSummary, attachments, remoteAet); + + if (status != StoreStatus_Success) + { + storage_.Remove(dicomInfo.GetUuid()); + storage_.Remove(jsonInfo.GetUuid()); + } + + switch (status) + { + case StoreStatus_Success: + LOG(INFO) << "New instance stored"; + break; + + case StoreStatus_AlreadyStored: + LOG(INFO) << "Already stored"; + break; + + case StoreStatus_Failure: + LOG(ERROR) << "Store failure"; + break; + } + + return status; + } + + + void ServerContext::AnswerFile(RestApiOutput& output, + const std::string& instancePublicId, + FileContentType content) + { + FileInfo attachment; + if (!index_.LookupAttachment(attachment, instancePublicId, content)) + { + throw OrthancException(ErrorCode_InternalError); + } + + accessor_.SetCompressionForNextOperations(attachment.GetCompressionType()); + std::auto_ptr<HttpFileSender> sender(accessor_.ConstructHttpFileSender(attachment.GetUuid())); + output.AnswerFile(*sender); + } + + + void ServerContext::ReadJson(Json::Value& result, + const std::string& instancePublicId) + { + std::string s; + ReadFile(s, instancePublicId, FileContentType_Json); + + Json::Reader reader; + if (!reader.parse(s, result)) + { + throw OrthancException("Corrupted JSON file"); + } + } + + + void ServerContext::ReadFile(std::string& result, + const std::string& instancePublicId, + FileContentType content) + { + FileInfo attachment; + if (!index_.LookupAttachment(attachment, instancePublicId, content)) + { + throw OrthancException(ErrorCode_InternalError); + } + + 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)); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerContext.h Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,111 @@ +/** + * 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 "../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 +{ + /** + * This class is responsible for maintaining the storage area on the + * filesystem (including compression), as well as the index of the + * DICOM store. It implements the required locking mechanisms. + **/ + 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); + + ServerIndex& GetIndex() + { + return index_; + } + + void SetCompressionEnabled(bool enabled); + + bool IsCompressionEnabled() const + { + return compressionEnabled_; + } + + void RemoveFile(const std::string& fileUuid); + + StoreStatus Store(const char* dicomFile, + size_t dicomSize, + const DicomMap& dicomSummary, + const Json::Value& dicomJson, + const std::string& remoteAet); + + void AnswerFile(RestApiOutput& output, + const std::string& instancePublicId, + FileContentType content); + + void ReadJson(Json::Value& result, + const std::string& instancePublicId); + + // TODO CACHING MECHANISM AT THIS POINT + void ReadFile(std::string& result, + const std::string& instancePublicId, + FileContentType content); + + // TODO IMPLEMENT MULTITHREADING FOR THIS METHOD + ParsedDicomFile& GetDicomFile(const std::string& instancePublicId); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerEnumerations.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,182 @@ +/** + * 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 "ServerEnumerations.h" + +#include "../Core/OrthancException.h" + +namespace Orthanc +{ + const char* ToString(ResourceType type) + { + switch (type) + { + case ResourceType_Patient: + return "Patient"; + + case ResourceType_Study: + return "Study"; + + case ResourceType_Series: + return "Series"; + + case ResourceType_Instance: + return "Instance"; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + std::string GetBasePath(ResourceType type, + const std::string& publicId) + { + switch (type) + { + case ResourceType_Patient: + return "/patients/" + publicId; + + case ResourceType_Study: + return "/studies/" + publicId; + + case ResourceType_Series: + return "/series/" + publicId; + + case ResourceType_Instance: + return "/instances/" + publicId; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + const char* ToString(SeriesStatus status) + { + switch (status) + { + case SeriesStatus_Complete: + return "Complete"; + + case SeriesStatus_Missing: + return "Missing"; + + case SeriesStatus_Inconsistent: + return "Inconsistent"; + + case SeriesStatus_Unknown: + return "Unknown"; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + const char* ToString(StoreStatus status) + { + switch (status) + { + case StoreStatus_Success: + return "Success"; + + case StoreStatus_AlreadyStored: + return "AlreadyStored"; + + case StoreStatus_Failure: + return "Failure"; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + const char* ToString(ChangeType type) + { + switch (type) + { + case ChangeType_CompletedSeries: + return "CompletedSeries"; + + case ChangeType_NewInstance: + return "NewInstance"; + + case ChangeType_NewPatient: + return "NewPatient"; + + case ChangeType_NewSeries: + return "NewSeries"; + + case ChangeType_NewStudy: + return "NewStudy"; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + ResourceType GetParentResourceType(ResourceType type) + { + switch (type) + { + case ResourceType_Study: + return ResourceType_Patient; + + case ResourceType_Series: + return ResourceType_Study; + + case ResourceType_Instance: + return ResourceType_Series; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + ResourceType GetChildResourceType(ResourceType type) + { + switch (type) + { + case ResourceType_Patient: + return ResourceType_Study; + + case ResourceType_Study: + return ResourceType_Series; + + case ResourceType_Series: + return ResourceType_Instance; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerEnumerations.h Fri Dec 14 11:24:24 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/>. + **/ + +#pragma once + +#include <string> + +namespace Orthanc +{ + enum SeriesStatus + { + SeriesStatus_Complete, + SeriesStatus_Missing, + SeriesStatus_Inconsistent, + SeriesStatus_Unknown + }; + + enum StoreStatus + { + StoreStatus_Success, + StoreStatus_AlreadyStored, + StoreStatus_Failure + }; + + + /** + * WARNING: Do not change the explicit values in the enumerations + * below this point. This would result in incompatible databases + * between versions of Orthanc! + **/ + + enum GlobalProperty + { + GlobalProperty_DatabaseSchemaVersion = 1, + GlobalProperty_FlushSleep = 2 + }; + + enum ResourceType + { + ResourceType_Patient = 1, + ResourceType_Study = 2, + ResourceType_Series = 3, + ResourceType_Instance = 4 + }; + + enum MetadataType + { + MetadataType_Instance_IndexInSeries = 1, + MetadataType_Instance_ReceptionDate = 2, + MetadataType_Instance_RemoteAet = 3, + MetadataType_Series_ExpectedNumberOfInstances = 4 + }; + + enum ChangeType + { + ChangeType_CompletedSeries = 1, + ChangeType_NewInstance = 2, + ChangeType_NewPatient = 3, + ChangeType_NewSeries = 4, + ChangeType_NewStudy = 5 + }; + + std::string GetBasePath(ResourceType type, + const std::string& publicId); + + const char* ToString(ResourceType type); + + const char* ToString(SeriesStatus status); + + const char* ToString(StoreStatus status); + + const char* ToString(ChangeType type); + + ResourceType GetParentResourceType(ResourceType type); + + ResourceType GetChildResourceType(ResourceType type); +}
--- a/OrthancServer/ServerIndex.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/ServerIndex.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -20,8 +32,6 @@ #include "ServerIndex.h" -using namespace Orthanc; - #ifndef NOMINMAX #define NOMINMAX #endif @@ -32,6 +42,7 @@ #include "../Core/DicomFormat/DicomArray.h" #include "../Core/SQLite/Transaction.h" #include "FromDcmtkBridge.h" +#include "ServerContext.h" #include <boost/lexical_cast.hpp> #include <stdio.h> @@ -41,437 +52,166 @@ { namespace Internals { - class DeleteFromFileStorageFunction : public SQLite::IScalarFunction + class ServerIndexListener : public IServerIndexListener { private: - FileStorage fileStorage_; + ServerContext& context_; + bool hasRemainingLevel_; + ResourceType remainingType_; + std::string remainingPublicId_; + std::list<std::string> pendingFilesToRemove_; + uint64_t sizeOfFilesToRemove_; public: - DeleteFromFileStorageFunction(const std::string& path) : - fileStorage_(path) + ServerIndexListener(ServerContext& context) : + context_(context) { + Reset(); + assert(ResourceType_Patient < ResourceType_Study && + ResourceType_Study < ResourceType_Series && + ResourceType_Series < ResourceType_Instance); } - virtual const char* GetName() const + void Reset() { - return "DeleteFromFileStorage"; + sizeOfFilesToRemove_ = 0; + hasRemainingLevel_ = false; + pendingFilesToRemove_.clear(); } - virtual unsigned int GetCardinality() const + uint64_t GetSizeOfFilesToRemove() { - return 1; + return sizeOfFilesToRemove_; } - virtual void Compute(SQLite::FunctionContext& context) + void CommitFilesToRemove() { - std::string fileUuid = context.GetStringValue(0); - LOG(INFO) << "Removing file [" << fileUuid << "]"; - if (Toolbox::IsUuid(fileUuid)) + for (std::list<std::string>::iterator + it = pendingFilesToRemove_.begin(); + it != pendingFilesToRemove_.end(); it++) { - fileStorage_.Remove(fileUuid); + context_.RemoveFile(*it); } } - }; + virtual void SignalRemainingAncestor(ResourceType parentType, + const std::string& publicId) + { + LOG(INFO) << "Remaining ancestor \"" << publicId << "\" (" << parentType << ")"; - class SignalDeletedLevelFunction : public SQLite::IScalarFunction - { - private: - int remainingLevel_; - std::string remainingLevelUuid_; + if (hasRemainingLevel_) + { + if (parentType < remainingType_) + { + remainingType_ = parentType; + remainingPublicId_ = publicId; + } + } + else + { + hasRemainingLevel_ = true; + remainingType_ = parentType; + remainingPublicId_ = publicId; + } + } - public: - void Clear() + virtual void SignalFileDeleted(const FileInfo& info) { - remainingLevel_ = std::numeric_limits<int>::max(); + assert(Toolbox::IsUuid(info.GetUuid())); + pendingFilesToRemove_.push_back(info.GetUuid()); + sizeOfFilesToRemove_ += info.GetCompressedSize(); } bool HasRemainingLevel() const { - return (remainingLevel_ != 0 && - remainingLevel_ != std::numeric_limits<int>::max()); + return hasRemainingLevel_; } - const std::string& GetRemainingLevelUuid() const - { - assert(HasRemainingLevel()); - return remainingLevelUuid_; - } - - const char* GetRemainingLevelType() const + ResourceType GetRemainingType() const { assert(HasRemainingLevel()); - switch (remainingLevel_) - { - case 1: - return "patient"; - case 2: - return "study"; - case 3: - return "series"; - default: - throw OrthancException(ErrorCode_InternalError); - } + return remainingType_; } - virtual const char* GetName() const - { - return "SignalDeletedLevel"; - } - - virtual unsigned int GetCardinality() const + const std::string& GetRemainingPublicId() const { - return 2; - } - - virtual void Compute(SQLite::FunctionContext& context) - { - int level = context.GetIntValue(0); - if (level < remainingLevel_) - { - remainingLevel_ = level; - remainingLevelUuid_ = context.GetStringValue(1); - } - - //printf("deleted level [%d] [%s]\n", level, context.GetStringValue(1).c_str()); - } + assert(HasRemainingLevel()); + return remainingPublicId_; + } }; } - void ServerIndex::StoreMainDicomTags(const std::string& uuid, - const DicomMap& map) + class ServerIndex::Transaction { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)"); + private: + ServerIndex& index_; + std::auto_ptr<SQLite::Transaction> transaction_; + bool isCommitted_; - DicomArray flattened(map); - for (size_t i = 0; i < flattened.GetSize(); i++) + public: + Transaction(ServerIndex& index) : + index_(index), + isCommitted_(false) { - s.Reset(); - s.BindString(0, uuid); - s.BindInt(1, flattened.GetElement(i).GetTag().GetGroup()); - s.BindInt(2, flattened.GetElement(i).GetTag().GetElement()); - s.BindString(3, flattened.GetElement(i).GetValue().AsString()); - s.Run(); - } - } + assert(index_.currentStorageSize_ == index_.db_->GetTotalCompressedSize()); - bool ServerIndex::GetMainDicomStringTag(std::string& result, - const std::string& uuid, - const DicomTag& tag) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT * FROM MainDicomTags WHERE uuid=? AND tagGroup=? AND tagElement=?"); - s.BindString(0, uuid); - s.BindInt(1, tag.GetGroup()); - s.BindInt(2, tag.GetElement()); - if (!s.Step()) - { - return false; + index_.listener_->Reset(); + transaction_.reset(index_.db_->StartTransaction()); + transaction_->Begin(); } - result = s.ColumnString(0); - return true; - } + void Commit(uint64_t sizeOfAddedFiles) + { + if (!isCommitted_) + { + transaction_->Commit(); - bool ServerIndex::GetMainDicomIntTag(int& result, - const std::string& uuid, - const DicomTag& tag) - { - std::string s; - if (!GetMainDicomStringTag(s, uuid, tag)) - { - return false; - } + // 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; - try - { - result = boost::lexical_cast<int>(s); - return true; + assert(index_.currentStorageSize_ >= index_.listener_->GetSizeOfFilesToRemove()); + index_.currentStorageSize_ -= index_.listener_->GetSizeOfFilesToRemove(); + + assert(index_.currentStorageSize_ == index_.db_->GetTotalCompressedSize()); + + isCommitted_ = true; + } } - catch (boost::bad_lexical_cast) - { - return false; - } - } - - bool ServerIndex::HasInstance(std::string& instanceUuid, - const std::string& dicomInstance) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT uuid FROM Instances WHERE dicomInstance=?"); - s.BindString(0, dicomInstance); - if (s.Step()) - { - instanceUuid = s.ColumnString(0); - return true; - } - else - { - return false; - } - } + }; - void ServerIndex::RecordChange(const std::string& resourceType, - const std::string& uuid) + bool ServerIndex::DeleteResource(Json::Value& target, + const std::string& uuid, + ResourceType expectedType) { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Changes VALUES(NULL, ?, ?)"); - s.BindString(0, resourceType); - s.BindString(1, uuid); - s.Run(); - } - - - std::string ServerIndex::CreateInstance(const std::string& parentSeriesUuid, - const std::string& dicomInstance, - const DicomMap& dicomSummary, - const std::string& fileUuid, - uint64_t fileSize, - const std::string& jsonUuid, - const std::string& distantAet) - { - std::string instanceUuid = Toolbox::GenerateUuid(); - - SQLite::Statement s2(db_, SQLITE_FROM_HERE, "INSERT INTO Resources VALUES(?, ?)"); - s2.BindString(0, instanceUuid); - s2.BindInt(1, ResourceType_Instance); - s2.Run(); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Instances VALUES(?, ?, ?, ?, ?, ?, ?, ?)"); - s.BindString(0, instanceUuid); - s.BindString(1, parentSeriesUuid); - s.BindString(2, dicomInstance); - s.BindString(3, fileUuid); - s.BindInt64(4, fileSize); - s.BindString(5, jsonUuid); - s.BindString(6, distantAet); + boost::mutex::scoped_lock lock(mutex_); + listener_->Reset(); - const DicomValue* indexInSeries; - if ((indexInSeries = dicomSummary.TestAndGetValue(DICOM_TAG_INSTANCE_NUMBER)) != NULL || - (indexInSeries = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGE_INDEX)) != NULL) - { - s.BindInt(7, boost::lexical_cast<unsigned int>(indexInSeries->AsString())); - } - else - { - s.BindNull(7); - } - - s.Run(); - - RecordChange("instances", instanceUuid); - - DicomMap dicom; - dicomSummary.ExtractInstanceInformation(dicom); - StoreMainDicomTags(instanceUuid, dicom); + Transaction t(*this); - return instanceUuid; - } - - void ServerIndex::RemoveInstance(const std::string& uuid) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Instances WHERE uuid=?"); - s.BindString(0, uuid); - s.Run(); - } - - bool ServerIndex::HasSeries(std::string& seriesUuid, - const std::string& dicomSeries) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT uuid FROM Series WHERE dicomSeries=?"); - s.BindString(0, dicomSeries); - if (s.Step()) - { - seriesUuid = s.ColumnString(0); - return true; - } - else + int64_t id; + ResourceType type; + if (!db_->LookupResource(uuid, id, type) || + expectedType != type) { return false; } - } - - std::string ServerIndex::CreateSeries(const std::string& parentStudyUuid, - const std::string& dicomSeries, - const DicomMap& dicomSummary) - { - std::string seriesUuid = Toolbox::GenerateUuid(); - - SQLite::Statement s2(db_, SQLITE_FROM_HERE, "INSERT INTO Resources VALUES(?, ?)"); - s2.BindString(0, seriesUuid); - s2.BindInt(1, ResourceType_Series); - s2.Run(); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Series VALUES(?, ?, ?, ?)"); - s.BindString(0, seriesUuid); - s.BindString(1, parentStudyUuid); - s.BindString(2, dicomSeries); - - const DicomValue* expectedNumberOfInstances; - if (//(expectedNumberOfInstances = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_FRAMES)) != NULL || - (expectedNumberOfInstances = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_SLICES)) != NULL || - //(expectedNumberOfInstances = dicomSummary.TestAndGetValue(DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES)) != NULL || - (expectedNumberOfInstances = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGES_IN_ACQUISITION)) != NULL) - { - s.BindInt(3, boost::lexical_cast<unsigned int>(expectedNumberOfInstances->AsString())); - } - else - { - s.BindNull(3); - } - - s.Run(); - - RecordChange("series", seriesUuid); - - DicomMap dicom; - dicomSummary.ExtractSeriesInformation(dicom); - StoreMainDicomTags(seriesUuid, dicom); - - return seriesUuid; - } - - bool ServerIndex::HasStudy(std::string& studyUuid, - const std::string& dicomStudy) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT uuid FROM Studies WHERE dicomStudy=?"); - s.BindString(0, dicomStudy); - if (s.Step()) - { - studyUuid = s.ColumnString(0); - return true; - } - else - { - return false; - } - } - - std::string ServerIndex::CreateStudy(const std::string& parentPatientUuid, - const std::string& dicomStudy, - const DicomMap& dicomSummary) - { - std::string studyUuid = Toolbox::GenerateUuid(); - - SQLite::Statement s2(db_, SQLITE_FROM_HERE, "INSERT INTO Resources VALUES(?, ?)"); - s2.BindString(0, studyUuid); - s2.BindInt(1, ResourceType_Study); - s2.Run(); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Studies VALUES(?, ?, ?)"); - s.BindString(0, studyUuid); - s.BindString(1, parentPatientUuid); - s.BindString(2, dicomStudy); - s.Run(); - - RecordChange("studies", studyUuid); - - DicomMap dicom; - dicomSummary.ExtractStudyInformation(dicom); - StoreMainDicomTags(studyUuid, dicom); - - return studyUuid; - } - - + + db_->DeleteResource(id); - bool ServerIndex::HasPatient(std::string& patientUuid, - const std::string& dicomPatientId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT uuid FROM Patients WHERE dicomPatientId=?"); - s.BindString(0, dicomPatientId); - if (s.Step()) - { - patientUuid = s.ColumnString(0); - return true; - } - else + if (listener_->HasRemainingLevel()) { - return false; - } - } - - std::string ServerIndex::CreatePatient(const std::string& patientId, - const DicomMap& dicomSummary) - { - std::string patientUuid = Toolbox::GenerateUuid(); - std::string dicomPatientId = dicomSummary.GetValue(DICOM_TAG_PATIENT_ID).AsString(); - - SQLite::Statement s2(db_, SQLITE_FROM_HERE, "INSERT INTO Resources VALUES(?, ?)"); - s2.BindString(0, patientUuid); - s2.BindInt(1, ResourceType_Patient); - s2.Run(); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Patients VALUES(?, ?)"); - s.BindString(0, patientUuid); - s.BindString(1, dicomPatientId); - s.Run(); - - RecordChange("patients", patientUuid); - - DicomMap dicom; - dicomSummary.ExtractPatientInformation(dicom); - StoreMainDicomTags(patientUuid, dicom); - - return patientUuid; - } - - - void ServerIndex::GetMainDicomTags(DicomMap& map, - const std::string& uuid) - { - map.Clear(); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM MainDicomTags WHERE uuid=?"); - s.BindString(0, uuid); - while (s.Step()) - { - map.SetValue(s.ColumnInt(1), - s.ColumnInt(2), - s.ColumnString(3)); - } - } - - void ServerIndex::MainDicomTagsToJson(Json::Value& target, - const std::string& uuid) - { - DicomMap map; - GetMainDicomTags(map, uuid); - target["MainDicomTags"] = Json::objectValue; - FromDcmtkBridge::ToJson(target["MainDicomTags"], map); - } - - - bool ServerIndex::DeleteInternal(Json::Value& target, - const std::string& uuid, - const std::string& tableName) - { - boost::mutex::scoped_lock scoped_lock(mutex_); - - deletedLevels_->Clear(); - - SQLite::Statement s(db_, "DELETE FROM " + tableName + " WHERE uuid=?"); - s.BindString(0, uuid); - - if (!s.Run()) - { - return false; - } - - if (db_.GetLastChangeCount() == 0) - { - // Nothing was deleted, inexistent UUID - return false; - } - - if (deletedLevels_->HasRemainingLevel()) - { - std::string type(deletedLevels_->GetRemainingLevelType()); - const std::string& uuid = deletedLevels_->GetRemainingLevelUuid(); + ResourceType type = listener_->GetRemainingType(); + const std::string& uuid = listener_->GetRemainingPublicId(); target["RemainingAncestor"] = Json::Value(Json::objectValue); - target["RemainingAncestor"]["Path"] = "/" + type + "/" + uuid; - target["RemainingAncestor"]["Type"] = type; + target["RemainingAncestor"]["Path"] = GetBasePath(type, uuid); + target["RemainingAncestor"]["Type"] = ToString(type); target["RemainingAncestor"]["ID"] = uuid; } else @@ -479,177 +219,280 @@ target["RemainingAncestor"] = Json::nullValue; } + t.Commit(0); + return true; } - ServerIndex::ServerIndex(const std::string& storagePath) + static void FlushThread(DatabaseWrapper* db, + boost::mutex* mutex, + unsigned int sleep) { - boost::filesystem::path p = storagePath; - - try - { - boost::filesystem::create_directories(storagePath); - } - catch (boost::filesystem::filesystem_error) - { - } + LOG(INFO) << "Starting the database flushing thread (sleep = " << sleep << ")"; - p /= "index"; - db_.Open(p.string()); - db_.Register(new Internals::DeleteFromFileStorageFunction(storagePath)); - deletedLevels_ = (Internals::SignalDeletedLevelFunction*) - db_.Register(new Internals::SignalDeletedLevelFunction); - - if (!db_.DoesTableExist("GlobalProperties")) + while (1) { - LOG(INFO) << "Creating the database"; - std::string query; - EmbeddedResources::GetFileResource(query, EmbeddedResources::PREPARE_DATABASE); - db_.Execute(query); + boost::this_thread::sleep(boost::posix_time::seconds(sleep)); + boost::mutex::scoped_lock lock(*mutex); + db->FlushToDisk(); } } - StoreStatus ServerIndex::Store(std::string& instanceUuid, - const DicomMap& dicomSummary, - const std::string& fileUuid, - uint64_t uncompressedFileSize, - const std::string& jsonUuid, - const std::string& distantAet) + ServerIndex::ServerIndex(ServerContext& context, + const std::string& dbPath) : + maximumStorageSize_(0), + maximumPatients_(0) { - boost::mutex::scoped_lock scoped_lock(mutex_); + listener_.reset(new Internals::ServerIndexListener(context)); + + if (dbPath == ":memory:") + { + db_.reset(new DatabaseWrapper(*listener_)); + } + else + { + boost::filesystem::path p = dbPath; + + try + { + boost::filesystem::create_directories(p); + } + catch (boost::filesystem::filesystem_error) + { + } + + 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(); - std::string dicomPatientId = dicomSummary.GetValue(DICOM_TAG_PATIENT_ID).AsString(); - std::string dicomInstance = dicomSummary.GetValue(DICOM_TAG_SOP_INSTANCE_UID).AsString(); - std::string dicomSeries = dicomSummary.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).AsString(); - std::string dicomStudy = dicomSummary.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString(); + unsigned int sleep; + try + { + std::string sleepString = db_->GetGlobalProperty(GlobalProperty_FlushSleep); + sleep = boost::lexical_cast<unsigned int>(sleepString); + } + catch (boost::bad_lexical_cast&) + { + // By default, wait for 10 seconds before flushing + sleep = 10; + } + + flushThread_ = boost::thread(FlushThread, db_.get(), &mutex_, sleep); + } + + + ServerIndex::~ServerIndex() + { + LOG(INFO) << "Stopping the database flushing thread"; + /*flushThread_.terminate(); + flushThread_.join();*/ + } + + + StoreStatus ServerIndex::Store(const DicomMap& dicomSummary, + const Attachments& attachments, + const std::string& remoteAet) + { + boost::mutex::scoped_lock lock(mutex_); + listener_->Reset(); + + DicomInstanceHasher hasher(dicomSummary); try { - SQLite::Transaction t(db_); - t.Begin(); + Transaction t(*this); + + int64_t patient, study, series, instance; + ResourceType type; + bool isNewSeries = false; - if (HasInstance(instanceUuid, dicomInstance)) + // Do nothing if the instance already exists + if (db_->LookupResource(hasher.HashInstance(), patient, type)) { + assert(type == ResourceType_Instance); return StoreStatus_AlreadyStored; - // TODO: Check consistency? + } + + // 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(); } - std::string patientUuid; - if (HasPatient(patientUuid, dicomPatientId)) + Recycle(instanceSize, hasher.HashPatient()); + + // Create the instance + instance = db_->CreateResource(hasher.HashInstance(), ResourceType_Instance); + + DicomMap dicom; + dicomSummary.ExtractInstanceInformation(dicom); + db_->SetMainDicomTags(instance, dicom); + + // Create the patient/study/series/instance hierarchy + if (!db_->LookupResource(hasher.HashSeries(), series, type)) { - // TODO: Check consistency? + // This is a new series + isNewSeries = true; + series = db_->CreateResource(hasher.HashSeries(), ResourceType_Series); + dicomSummary.ExtractSeriesInformation(dicom); + db_->SetMainDicomTags(series, dicom); + db_->AttachChild(series, instance); + + if (!db_->LookupResource(hasher.HashStudy(), study, type)) + { + // This is a new study + study = db_->CreateResource(hasher.HashStudy(), ResourceType_Study); + dicomSummary.ExtractStudyInformation(dicom); + db_->SetMainDicomTags(study, dicom); + db_->AttachChild(study, series); + + if (!db_->LookupResource(hasher.HashPatient(), patient, type)) + { + // This is a new patient + patient = db_->CreateResource(hasher.HashPatient(), ResourceType_Patient); + dicomSummary.ExtractPatientInformation(dicom); + db_->SetMainDicomTags(patient, dicom); + db_->AttachChild(patient, study); + } + else + { + assert(type == ResourceType_Patient); + db_->AttachChild(patient, study); + } + } + else + { + assert(type == ResourceType_Study); + db_->AttachChild(study, series); + } } else { - patientUuid = CreatePatient(dicomPatientId, dicomSummary); + assert(type == ResourceType_Series); + db_->AttachChild(series, instance); } - std::string studyUuid; - if (HasStudy(studyUuid, dicomStudy)) + // Attach the files to the newly created instance + for (Attachments::const_iterator it = attachments.begin(); + it != attachments.end(); it++) { - // TODO: Check consistency? - } - else - { - studyUuid = CreateStudy(patientUuid, dicomStudy, dicomSummary); + db_->AddAttachment(instance, *it); } - std::string seriesUuid; - if (HasSeries(seriesUuid, dicomSeries)) + // Attach the metadata + db_->SetMetadata(instance, MetadataType_Instance_ReceptionDate, Toolbox::GetNowIsoString()); + db_->SetMetadata(instance, MetadataType_Instance_RemoteAet, remoteAet); + + const DicomValue* value; + if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_INSTANCE_NUMBER)) != NULL || + (value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGE_INDEX)) != NULL) { - // TODO: Check consistency? - } - else - { - seriesUuid = CreateSeries(studyUuid, dicomSeries, dicomSummary); + db_->SetMetadata(instance, MetadataType_Instance_IndexInSeries, value->AsString()); } - instanceUuid = CreateInstance(seriesUuid, dicomInstance, dicomSummary, fileUuid, - uncompressedFileSize, jsonUuid, distantAet); - - t.Commit(); + if (isNewSeries) + { + if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_SLICES)) != NULL || + (value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGES_IN_ACQUISITION)) != NULL || + (value = dicomSummary.TestAndGetValue(DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES)) != NULL) + { + db_->SetMetadata(series, MetadataType_Series_ExpectedNumberOfInstances, value->AsString()); + } + } + + // Check whether the series of this new instance is now completed + SeriesStatus seriesStatus = GetSeriesStatus(series); + if (seriesStatus == SeriesStatus_Complete) + { + db_->LogChange(ChangeType_CompletedSeries, series, ResourceType_Series); + } + + t.Commit(instanceSize); + return StoreStatus_Success; - //t.Rollback(); } catch (OrthancException& e) { - LOG(ERROR) << "EXCEPTION [" << e.What() << "]" << " " << db_.GetErrorMessage(); + LOG(ERROR) << "EXCEPTION [" << e.What() << "]" + << " (SQLite status: " << db_->GetErrorMessage() << ")"; } return StoreStatus_Failure; } - StoreStatus ServerIndex::Store(std::string& instanceUuid, - FileStorage& storage, - const char* dicomFile, - size_t dicomSize, - const DicomMap& dicomSummary, - const Json::Value& dicomJson, - const std::string& distantAet) + void ServerIndex::ComputeStatistics(Json::Value& target) { - std::string fileUuid = storage.Create(dicomFile, dicomSize); - std::string jsonUuid = storage.Create(dicomJson.toStyledString()); - StoreStatus status = Store(instanceUuid, dicomSummary, fileUuid, - dicomSize, jsonUuid, distantAet); + static const uint64_t MB = 1024 * 1024; - if (status != StoreStatus_Success) - { - storage.Remove(fileUuid); - storage.Remove(jsonUuid); - } + boost::mutex::scoped_lock lock(mutex_); + target = Json::objectValue; - switch (status) - { - case StoreStatus_Success: - LOG(INFO) << "New instance stored: " << GetTotalSize() << " bytes"; - break; - - case StoreStatus_AlreadyStored: - LOG(INFO) << "Already stored"; - break; + 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); + target["TotalDiskSpaceMB"] = boost::lexical_cast<unsigned int>(cs / MB); + target["TotalUncompressedSizeMB"] = boost::lexical_cast<unsigned int>(us / MB); - case StoreStatus_Failure: - LOG(INFO) << "Store failure"; - break; - } - - return status; - } - - uint64_t ServerIndex::GetTotalSize() - { - boost::mutex::scoped_lock scoped_lock(mutex_); - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(fileSize) FROM Instances"); - s.Run(); - return s.ColumnInt64(0); - } + target["CountPatients"] = static_cast<unsigned int>(db_->GetResourceCount(ResourceType_Patient)); + target["CountStudies"] = static_cast<unsigned int>(db_->GetResourceCount(ResourceType_Study)); + target["CountSeries"] = static_cast<unsigned int>(db_->GetResourceCount(ResourceType_Series)); + target["CountInstances"] = static_cast<unsigned int>(db_->GetResourceCount(ResourceType_Instance)); + } - SeriesStatus ServerIndex::GetSeriesStatus(const std::string& seriesUuid) + + SeriesStatus ServerIndex::GetSeriesStatus(int id) { - SQLite::Statement s1(db_, SQLITE_FROM_HERE, "SELECT expectedNumberOfInstances FROM Series WHERE uuid=?"); - s1.BindString(0, seriesUuid); - if (!s1.Step() || s1.ColumnIsNull(0)) + // Get the expected number of instances in this series (from the metadata) + std::string s = db_->GetMetadata(id, MetadataType_Series_ExpectedNumberOfInstances); + + size_t expected; + try + { + expected = boost::lexical_cast<size_t>(s); + if (expected < 0) + { + return SeriesStatus_Unknown; + } + } + catch (boost::bad_lexical_cast&) { return SeriesStatus_Unknown; } - int numberOfInstances = s1.ColumnInt(0); - if (numberOfInstances < 0) - { - return SeriesStatus_Unknown; - } + // Loop over the instances of this series + std::list<int64_t> children; + db_->GetChildrenInternalId(children, id); - std::set<int> instances; - SQLite::Statement s2(db_, SQLITE_FROM_HERE, "SELECT indexInSeries FROM Instances WHERE parentSeries=?"); - s2.BindString(0, seriesUuid); - while (s2.Step()) + std::set<size_t> instances; + for (std::list<int64_t>::const_iterator + it = children.begin(); it != children.end(); it++) { - int index = s2.ColumnInt(0); - if (index <= 0 || index > numberOfInstances) + // Get the index of this instance in the series + s = db_->GetMetadata(*it, MetadataType_Instance_IndexInSeries); + size_t index; + try + { + index = boost::lexical_cast<size_t>(s); + } + catch (boost::bad_lexical_cast&) + { + return SeriesStatus_Unknown; + } + + if (index <= 0 || index > expected) { // Out-of-range instance index return SeriesStatus_Inconsistent; @@ -664,202 +507,183 @@ instances.insert(index); } - for (int i = 1; i <= numberOfInstances; i++) + if (instances.size() == expected) { - if (instances.find(i) == instances.end()) - { - return SeriesStatus_Missing; - } - } - - return SeriesStatus_Complete; - } - - - - bool ServerIndex::GetInstance(Json::Value& result, - const std::string& instanceUuid) - { - assert(result.type() == Json::objectValue); - boost::mutex::scoped_lock scoped_lock(mutex_); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT parentSeries, dicomInstance, fileSize, fileUuid, indexInSeries FROM Instances WHERE uuid=?"); - s.BindString(0, instanceUuid); - if (!s.Step()) - { - return false; + return SeriesStatus_Complete; } else { - result["ID"] = instanceUuid; - result["ParentSeries"] = s.ColumnString(0); - result["FileSize"] = s.ColumnInt(2); // TODO switch to 64bit with JsonCpp 0.6? - result["FileUuid"] = s.ColumnString(3); - MainDicomTagsToJson(result, instanceUuid); - - if (s.ColumnIsNull(4)) - { - result["IndexInSeries"] = Json::nullValue; - } - else - { - result["IndexInSeries"] = s.ColumnInt(4); - } - - return true; + return SeriesStatus_Missing; } } - bool ServerIndex::GetSeries(Json::Value& result, - const std::string& seriesUuid) + + void ServerIndex::MainDicomTagsToJson(Json::Value& target, + int64_t resourceId) { - assert(result.type() == Json::objectValue); - boost::mutex::scoped_lock scoped_lock(mutex_); + DicomMap tags; + db_->GetMainDicomTags(tags, resourceId); + target["MainDicomTags"] = Json::objectValue; + FromDcmtkBridge::ToJson(target["MainDicomTags"], tags); + } - SQLite::Statement s1(db_, SQLITE_FROM_HERE, "SELECT parentStudy, dicomSeries, expectedNumberOfInstances FROM Series WHERE uuid=?"); - s1.BindString(0, seriesUuid); - if (!s1.Step()) + bool ServerIndex::LookupResource(Json::Value& result, + const std::string& publicId, + ResourceType expectedType) + { + result = Json::objectValue; + + boost::mutex::scoped_lock lock(mutex_); + + // Lookup for the requested resource + int64_t id; + ResourceType type; + if (!db_->LookupResource(publicId, id, type) || + type != expectedType) { return false; } - result["ID"] = seriesUuid; - result["ParentStudy"] = s1.ColumnString(0); - MainDicomTagsToJson(result, seriesUuid); - - Json::Value instances(Json::arrayValue); - SQLite::Statement s2(db_, SQLITE_FROM_HERE, "SELECT uuid FROM Instances WHERE parentSeries=?"); - s2.BindString(0, seriesUuid); - while (s2.Step()) + // Find the parent resource (if it exists) + if (type != ResourceType_Patient) { - instances.append(s2.ColumnString(0)); - } - - result["Instances"] = instances; + int64_t parentId; + if (!db_->LookupParent(parentId, id)) + { + throw OrthancException(ErrorCode_InternalError); + } + + std::string parent = db_->GetPublicId(parentId); - if (s1.ColumnIsNull(2)) - { - result["ExpectedNumberOfInstances"] = Json::nullValue; - } - else - { - result["ExpectedNumberOfInstances"] = s1.ColumnInt(2); + switch (type) + { + case ResourceType_Study: + result["ParentPatient"] = parent; + break; + + case ResourceType_Series: + result["ParentStudy"] = parent; + break; + + case ResourceType_Instance: + result["ParentSeries"] = parent; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } } - SeriesStatus status = GetSeriesStatus(seriesUuid); + // List the children resources + std::list<std::string> children; + db_->GetChildrenPublicId(children, id); - switch (status) + if (type != ResourceType_Instance) { - case SeriesStatus_Complete: - result["Status"] = "Complete"; - break; + Json::Value c = Json::arrayValue; + + for (std::list<std::string>::const_iterator + it = children.begin(); it != children.end(); it++) + { + c.append(*it); + } + + switch (type) + { + case ResourceType_Patient: + result["Studies"] = c; + break; + + case ResourceType_Study: + result["Series"] = c; + break; + + case ResourceType_Series: + result["Instances"] = c; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + // Set the resource type + switch (type) + { + case ResourceType_Patient: + result["Type"] = "Patient"; + break; + + case ResourceType_Study: + result["Type"] = "Study"; + break; - case SeriesStatus_Missing: - result["Status"] = "Missing"; - break; + case ResourceType_Series: + { + result["Type"] = "Series"; + result["Status"] = ToString(GetSeriesStatus(id)); + + int i; + if (db_->GetMetadataAsInteger(i, id, MetadataType_Series_ExpectedNumberOfInstances)) + result["ExpectedNumberOfInstances"] = i; + else + result["ExpectedNumberOfInstances"] = Json::nullValue; + + break; + } + + case ResourceType_Instance: + { + result["Type"] = "Instance"; - case SeriesStatus_Inconsistent: - result["Status"] = "Inconsistent"; - break; + FileInfo attachment; + if (!db_->LookupAttachment(attachment, id, FileContentType_Dicom)) + { + throw OrthancException(ErrorCode_InternalError); + } + + result["FileSize"] = static_cast<unsigned int>(attachment.GetUncompressedSize()); + result["FileUuid"] = attachment.GetUuid(); - default: - case SeriesStatus_Unknown: - result["Status"] = "Unknown"; - break; + int i; + if (db_->GetMetadataAsInteger(i, id, MetadataType_Instance_IndexInSeries)) + result["IndexInSeries"] = i; + else + result["IndexInSeries"] = Json::nullValue; + + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); } + // Record the remaining information + result["ID"] = publicId; + MainDicomTagsToJson(result, id); + return true; } - bool ServerIndex::GetStudy(Json::Value& result, - const std::string& studyUuid) + bool ServerIndex::LookupAttachment(FileInfo& attachment, + const std::string& instanceUuid, + FileContentType contentType) { - assert(result.type() == Json::objectValue); - boost::mutex::scoped_lock scoped_lock(mutex_); - - SQLite::Statement s1(db_, SQLITE_FROM_HERE, "SELECT parentPatient, dicomStudy FROM Studies WHERE uuid=?"); - s1.BindString(0, studyUuid); - if (!s1.Step()) - { - return false; - } - - result["ID"] = studyUuid; - result["ParentPatient"] = s1.ColumnString(0); - MainDicomTagsToJson(result, studyUuid); + boost::mutex::scoped_lock lock(mutex_); - Json::Value series(Json::arrayValue); - SQLite::Statement s2(db_, SQLITE_FROM_HERE, "SELECT uuid FROM Series WHERE parentStudy=?"); - s2.BindString(0, studyUuid); - while (s2.Step()) + int64_t id; + ResourceType type; + if (!db_->LookupResource(instanceUuid, id, type) || + type != ResourceType_Instance) { - series.append(s2.ColumnString(0)); - } - - result["Series"] = series; - return true; - } - - - bool ServerIndex::GetPatient(Json::Value& result, - const std::string& patientUuid) - { - assert(result.type() == Json::objectValue); - boost::mutex::scoped_lock scoped_lock(mutex_); - - SQLite::Statement s1(db_, SQLITE_FROM_HERE, "SELECT dicomPatientId FROM Patients WHERE uuid=?"); - s1.BindString(0, patientUuid); - if (!s1.Step()) - { - return false; + throw OrthancException(ErrorCode_InternalError); } - result["ID"] = patientUuid; - MainDicomTagsToJson(result, patientUuid); - - Json::Value studies(Json::arrayValue); - SQLite::Statement s2(db_, SQLITE_FROM_HERE, "SELECT uuid FROM Studies WHERE parentPatient=?"); - s2.BindString(0, patientUuid); - while (s2.Step()) + if (db_->LookupAttachment(attachment, id, contentType)) { - studies.append(s2.ColumnString(0)); - } - - result["Studies"] = studies; - return true; - } - - - bool ServerIndex::GetJsonFile(std::string& fileUuid, - const std::string& instanceUuid) - { - boost::mutex::scoped_lock scoped_lock(mutex_); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT jsonUuid FROM Instances WHERE uuid=?"); - s.BindString(0, instanceUuid); - if (s.Step()) - { - fileUuid = s.ColumnString(0); - return true; - } - else - { - return false; - } - } - - bool ServerIndex::GetDicomFile(std::string& fileUuid, - const std::string& instanceUuid) - { - boost::mutex::scoped_lock scoped_lock(mutex_); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT fileUuid FROM Instances WHERE uuid=?"); - s.BindString(0, instanceUuid); - if (s.Step()) - { - fileUuid = s.ColumnString(0); + assert(attachment.GetContentType() == contentType); return true; } else @@ -869,83 +693,273 @@ } + void ServerIndex::GetAllUuids(Json::Value& target, - const std::string& tableName) + ResourceType resourceType) { - assert(target.type() == Json::arrayValue); - boost::mutex::scoped_lock scoped_lock(mutex_); - - std::string query = "SELECT uuid FROM " + tableName; - SQLite::Statement s(db_, query); - while (s.Step()) - { - target.append(s.ColumnString(0)); - } + boost::mutex::scoped_lock lock(mutex_); + db_->GetAllPublicIds(target, resourceType); } bool ServerIndex::GetChanges(Json::Value& target, - int64_t since, - const std::string& filter, + int64_t since, unsigned int maxResults) { - assert(target.type() == Json::objectValue); - boost::mutex::scoped_lock scoped_lock(mutex_); + boost::mutex::scoped_lock lock(mutex_); + db_->GetChanges(target, since, maxResults); + return true; + } + + bool ServerIndex::GetLastChange(Json::Value& target) + { + boost::mutex::scoped_lock lock(mutex_); + db_->GetLastChange(target); + return true; + } + + void ServerIndex::LogExportedResource(const std::string& publicId, + const std::string& remoteModality) + { + boost::mutex::scoped_lock lock(mutex_); + + int64_t id; + ResourceType type; + if (!db_->LookupResource(publicId, id, type)) + { + throw OrthancException(ErrorCode_InternalError); + } + + std::string patientId; + std::string studyInstanceUid; + std::string seriesInstanceUid; + std::string sopInstanceUid; + + int64_t currentId = id; + ResourceType currentType = type; - if (filter.size() != 0 && - filter != "instances" && - filter != "series" && - filter != "studies" && - filter != "patients") + // Iteratively go up inside the patient/study/series/instance hierarchy + bool done = false; + while (!done) { - return false; + DicomMap map; + db_->GetMainDicomTags(map, currentId); + + switch (currentType) + { + 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_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; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + // If we have not reached the Patient level, find the parent of + // the current resource + if (!done) + { + assert(db_->LookupParent(currentId, currentId)); + } } - std::auto_ptr<SQLite::Statement> s; - if (filter.size() == 0) - { - s.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes WHERE seq>? " - "ORDER BY seq LIMIT ?")); - s->BindInt64(0, since); - s->BindInt(1, maxResults); + // No need for a SQLite::Transaction here, as we only insert 1 record + db_->LogExportedResource(type, + publicId, + remoteModality, + patientId, + studyInstanceUid, + seriesInstanceUid, + sopInstanceUid); + } + + + bool ServerIndex::GetExportedResources(Json::Value& target, + int64_t since, + unsigned int maxResults) + { + boost::mutex::scoped_lock lock(mutex_); + db_->GetExportedResources(target, since, maxResults); + return true; + } + + bool ServerIndex::GetLastExportedResource(Json::Value& target) + { + boost::mutex::scoped_lock lock(mutex_); + 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 { - s.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes WHERE seq>? " - "AND basePath=? ORDER BY seq LIMIT ?")); - s->BindInt64(0, since); - s->BindString(1, filter); - s->BindInt(2, maxResults); + LOG(WARNING) << "At most " << count << " patients will be stored"; } - - int64_t lastSeq = 0; - Json::Value results(Json::arrayValue); - while (s->Step()) - { - int64_t seq = s->ColumnInt64(0); - std::string basePath = s->ColumnString(1); - std::string uuid = s->ColumnString(2); + + StandaloneRecycling(); + } - if (filter.size() == 0 || - filter == basePath) - { - Json::Value change(Json::objectValue); - change["Seq"] = static_cast<int>(seq); // TODO JsonCpp in 64bit - change["BasePath"] = basePath; - change["ID"] = uuid; - results.append(change); - } + void ServerIndex::SetMaximumStorageSize(uint64_t size) + { + boost::mutex::scoped_lock lock(mutex_); + maximumStorageSize_ = size; - if (seq > lastSeq) - { - lastSeq = seq; - } + 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"; } - target["Results"] = results; - target["LastSeq"] = static_cast<int>(lastSeq); // TODO JsonCpp in 64bit + 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 true; + 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 Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/ServerIndex.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -21,187 +33,114 @@ #pragma once #include <boost/thread.hpp> +#include <boost/noncopyable.hpp> #include "../Core/SQLite/Connection.h" #include "../Core/DicomFormat/DicomMap.h" -#include "../Core/FileStorage.h" +#include "../Core/DicomFormat/DicomInstanceHasher.h" +#include "ServerEnumerations.h" + +#include "DatabaseWrapper.h" namespace Orthanc { - enum SeriesStatus - { - SeriesStatus_Complete, - SeriesStatus_Missing, - SeriesStatus_Inconsistent, - SeriesStatus_Unknown - }; - - - enum StoreStatus - { - StoreStatus_Success, - StoreStatus_AlreadyStored, - StoreStatus_Failure - }; - - - enum ResourceType - { - ResourceType_Patient = 1, - ResourceType_Study = 2, - ResourceType_Series = 3, - ResourceType_Instance = 4, - ResourceType_File = 5 - }; - + class ServerContext; namespace Internals { - class SignalDeletedLevelFunction; + class ServerIndexListener; } - - class ServerIndex + class ServerIndex : public boost::noncopyable { private: - SQLite::Connection db_; - boost::mutex mutex_; - - // DO NOT delete the following one, SQLite::Connection will do it automatically - Internals::SignalDeletedLevelFunction* deletedLevels_; + class Transaction; - void StoreMainDicomTags(const std::string& uuid, - const DicomMap& map); - - bool GetMainDicomStringTag(std::string& result, - const std::string& uuid, - const DicomTag& tag); + boost::mutex mutex_; + boost::thread flushThread_; - bool GetMainDicomIntTag(int& result, - const std::string& uuid, - const DicomTag& tag); - - bool HasInstance(std::string& instanceUuid, - const std::string& dicomInstance); + std::auto_ptr<Internals::ServerIndexListener> listener_; + std::auto_ptr<DatabaseWrapper> db_; - void RecordChange(const std::string& resourceType, - const std::string& uuid); - - std::string CreateInstance(const std::string& parentSeriesUuid, - const std::string& dicomInstance, - const DicomMap& dicomSummary, - const std::string& fileUuid, - uint64_t fileSize, - const std::string& jsonUuid, - const std::string& distantAet); + uint64_t currentStorageSize_; + uint64_t maximumStorageSize_; + unsigned int maximumPatients_; - void RemoveInstance(const std::string& uuid); - - bool HasSeries(std::string& seriesUuid, - const std::string& dicomSeries); + void MainDicomTagsToJson(Json::Value& result, + int64_t resourceId); - std::string CreateSeries(const std::string& parentStudyUuid, - const std::string& dicomSeries, - const DicomMap& dicomSummary); - - bool HasStudy(std::string& studyUuid, - const std::string& dicomStudy); - - std::string CreateStudy(const std::string& parentPatientUuid, - const std::string& dicomStudy, - const DicomMap& dicomSummary); + SeriesStatus GetSeriesStatus(int id); - bool HasPatient(std::string& patientUuid, - const std::string& dicomPatientId); - - std::string CreatePatient(const std::string& patientId, - const DicomMap& dicomSummary); + bool IsRecyclingNeeded(uint64_t instanceSize); - void GetMainDicomTags(DicomMap& map, - const std::string& uuid); + void Recycle(uint64_t instanceSize, + const std::string& newPatientId); - void MainDicomTagsToJson(Json::Value& target, - const std::string& uuid); - - bool DeleteInternal(Json::Value& target, - const std::string& uuid, - const std::string& tableName); + void StandaloneRecycling(); public: - ServerIndex(const std::string& storagePath); + typedef std::list<FileInfo> Attachments; - StoreStatus Store(std::string& instanceUuid, - const DicomMap& dicomSummary, - const std::string& fileUuid, - uint64_t uncompressedFileSize, - const std::string& jsonUuid, - const std::string& distantAet); + ServerIndex(ServerContext& context, + const std::string& dbPath); + + ~ServerIndex(); - StoreStatus Store(std::string& instanceUuid, - FileStorage& storage, - const char* dicomFile, - size_t dicomSize, - const DicomMap& dicomSummary, - const Json::Value& dicomJson, - const std::string& distantAet); + uint64_t GetMaximumStorageSize() const + { + return maximumStorageSize_; + } - uint64_t GetTotalSize(); + uint64_t GetMaximumPatientCount() const + { + return maximumPatients_; + } - SeriesStatus GetSeriesStatus(const std::string& seriesUuid); - + // "size == 0" means no limit on the storage size + void SetMaximumStorageSize(uint64_t size); - bool GetInstance(Json::Value& result, - const std::string& instanceUuid); + // "count == 0" means no limit on the number of patients + void SetMaximumPatientCount(unsigned int count); - bool GetSeries(Json::Value& result, - const std::string& seriesUuid); + StoreStatus Store(const DicomMap& dicomSummary, + const Attachments& attachments, + const std::string& remoteAet); - bool GetStudy(Json::Value& result, - const std::string& studyUuid); - - bool GetPatient(Json::Value& result, - const std::string& patientUuid); + void ComputeStatistics(Json::Value& target); - bool GetJsonFile(std::string& fileUuid, - const std::string& instanceUuid); + bool LookupResource(Json::Value& result, + const std::string& publicId, + ResourceType expectedType); - bool GetDicomFile(std::string& fileUuid, - const std::string& instanceUuid); + bool LookupAttachment(FileInfo& attachment, + const std::string& instanceUuid, + FileContentType contentType); void GetAllUuids(Json::Value& target, - const std::string& tableName); - - bool DeletePatient(Json::Value& target, - const std::string& patientUuid) - { - return DeleteInternal(target, patientUuid, "Patients"); - } + ResourceType resourceType); - bool DeleteStudy(Json::Value& target, - const std::string& studyUuid) - { - return DeleteInternal(target, studyUuid, "Studies"); - } - - bool DeleteSeries(Json::Value& target, - const std::string& seriesUuid) - { - return DeleteInternal(target, seriesUuid, "Series"); - } - - bool DeleteInstance(Json::Value& target, - const std::string& instanceUuid) - { - return DeleteInternal(target, instanceUuid, "Instances"); - } + bool DeleteResource(Json::Value& target, + const std::string& uuid, + ResourceType expectedType); bool GetChanges(Json::Value& target, int64_t since, - const std::string& filter, unsigned int maxResults); - /*bool GetAllInstances(std::list<std::string>& instancesUuid, - const std::string& uuid, - bool clear = true);*/ + bool GetLastChange(Json::Value& target); + + void LogExportedResource(const std::string& publicId, + const std::string& remoteModality); + + bool GetExportedResources(Json::Value& target, + int64_t since, + unsigned int maxResults); + + bool GetLastExportedResource(Json::Value& target); + + bool IsProtectedPatient(const std::string& publicId); + + void SetProtectedPatient(const std::string& publicId, + bool isProtected); }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerToolbox.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,85 @@ +/** + * 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 "ServerToolbox.h" + +#include "../Core/OrthancException.h" + +#include <cassert> + +namespace Orthanc +{ + void SimplifyTags(Json::Value& target, + const Json::Value& source) + { + assert(source.isObject()); + + target = Json::objectValue; + Json::Value::Members members = source.getMemberNames(); + + for (size_t i = 0; i < members.size(); i++) + { + const Json::Value& v = source[members[i]]; + const std::string& name = v["Name"].asString(); + const std::string& type = v["Type"].asString(); + + if (type == "String") + { + target[name] = v["Value"].asString(); + } + else if (type == "TooLong" || + type == "Null") + { + target[name] = Json::nullValue; + } + else if (type == "Sequence") + { + const Json::Value& array = v["Value"]; + assert(array.isArray()); + + Json::Value children = Json::arrayValue; + for (Json::Value::ArrayIndex i = 0; i < array.size(); i++) + { + Json::Value c; + SimplifyTags(c, array[i]); + children.append(c); + } + + target[name] = children; + } + else + { + assert(0); + } + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerToolbox.h Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,41 @@ +/** + * 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 <json/json.h> + +namespace Orthanc +{ + void SimplifyTags(Json::Value& target, + const Json::Value& source); +}
--- a/OrthancServer/ToDcmtkBridge.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/ToDcmtkBridge.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -21,7 +33,6 @@ #include "ToDcmtkBridge.h" #include <memory> -#include <dcmtk/dcmdata/dcelem.h> #include <dcmtk/dcmnet/diutil.h>
--- a/OrthancServer/ToDcmtkBridge.h Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/ToDcmtkBridge.h Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -23,7 +35,6 @@ #include "../Core/DicomFormat/DicomMap.h" #include <dcmtk/dcmdata/dcdatset.h> - namespace Orthanc { class ToDcmtkBridge
--- a/OrthancServer/main.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/OrthancServer/main.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,18 @@ * 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 @@ -20,7 +32,7 @@ #include "OrthancRestApi.h" -#include <stdio.h> +#include <fstream> #include <glog/logging.h> #include <boost/algorithm/string/predicate.hpp> @@ -29,22 +41,20 @@ #include "../Core/HttpServer/MongooseServer.h" #include "DicomProtocol/DicomServer.h" #include "OrthancInitialization.h" - +#include "ServerContext.h" using namespace Orthanc; + class MyDicomStore : public IStoreRequestHandler { private: - ServerIndex& index_; - FileStorage storage_; + ServerContext& context_; public: - MyDicomStore(ServerIndex& index, - const std::string& path) : - index_(index), - storage_(path) + MyDicomStore(ServerContext& context) : + context_(context) { } @@ -53,12 +63,10 @@ const Json::Value& dicomJson, const std::string& remoteAet) { - std::string instanceUuid; if (dicomFile.size() > 0) { - index_.Store(instanceUuid, storage_, - reinterpret_cast<const char*>(&dicomFile[0]), dicomFile.size(), - dicomSummary, dicomJson, remoteAet); + context_.Store(reinterpret_cast<const char*>(&dicomFile[0]), dicomFile.size(), + dicomSummary, dicomJson, remoteAet); } } }; @@ -67,20 +75,16 @@ class MyDicomStoreFactory : public IStoreRequestHandlerFactory { private: - ServerIndex& index_; - std::string path_; + ServerContext& context_; public: - MyDicomStoreFactory(ServerIndex& index, - const std::string& path) : - index_(index), - path_(path) + MyDicomStoreFactory(ServerContext& context) : context_(context) { } virtual IStoreRequestHandler* ConstructStoreRequestHandler() { - return new MyDicomStore(index_, path_); + return new MyDicomStore(context_); } void Done() @@ -90,21 +94,95 @@ }; +void PrintHelp(char* path) +{ + std::cout + << "Usage: " << path << " [OPTION]... [CONFIGURATION]" << std::endl + << "Orthanc, lightweight, RESTful DICOM server for healthcare and medical research." << std::endl + << std::endl + << "If no configuration file is given on the command line, a set of default " << std::endl + << "parameters is used. Please refer to the Orthanc homepage for the full " << std::endl + << "instructions about how to use Orthanc " << std::endl + << "<https://code.google.com/p/orthanc/wiki/OrthancCookbook>." << std::endl + << std::endl + << "Command-line options:" << std::endl + << " --help\t\tdisplay this help and exit" << std::endl + << " --logdir=[dir]\tdirectory where to store the log files" << std::endl + << "\t\t\t(if not used, the logs are dumped to stderr)" << std::endl + << " --config=[file]\tcreate a sample configuration file and exit" << std::endl + << " --trace\t\thighest verbosity in logs (for debug)" << std::endl + << " --verbose\t\tbe verbose in logs" << std::endl + << " --version\t\toutput version information and exit" << std::endl + << std::endl + << "Exit status:" << std::endl + << " 0 if OK," << std::endl + << " -1 if error (have a look at the logs)." << std::endl + << std::endl; +} +void PrintVersion(char* path) +{ + std::cout + << path << " " << ORTHANC_VERSION << std::endl + << "Copyright (C) 2012 Medical Physics Department, CHU of Liege (Belgium) " << std::endl + << "Licensing GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>, with OpenSSL exception." << std::endl + << "This is free software: you are free to change and redistribute it." << std::endl + << "There is NO WARRANTY, to the extent permitted by law." << std::endl + << std::endl + << "Written by Sebastien Jodogne <s.jodogne@gmail.com>" << std::endl; +} + int main(int argc, char* argv[]) { // Initialize Google's logging library. FLAGS_logtostderr = true; - + FLAGS_minloglevel = 1; + FLAGS_v = 0; + for (int i = 1; i < argc; i++) { + if (std::string(argv[i]) == "--help") + { + PrintHelp(argv[0]); + return 0; + } + + if (std::string(argv[i]) == "--version") + { + PrintVersion(argv[0]); + return 0; + } + + if (std::string(argv[i]) == "--verbose") + { + FLAGS_minloglevel = 0; + } + + if (std::string(argv[i]) == "--trace") + { + FLAGS_minloglevel = 0; + FLAGS_v = 1; + } + if (boost::starts_with(argv[i], "--logdir=")) { FLAGS_logtostderr = false; FLAGS_log_dir = std::string(argv[i]).substr(9); } + + if (boost::starts_with(argv[i], "--config=")) + { + std::string configurationSample; + GetFileResource(configurationSample, EmbeddedResources::CONFIGURATION_SAMPLE); + + std::string target = std::string(argv[i]).substr(9); + std::ofstream f(target.c_str()); + f << configurationSample; + f.close(); + return 0; + } } google::InitGoogleLogging("Orthanc"); @@ -132,9 +210,30 @@ OrthancInitialize(); } - std::string storageDirectory = GetGlobalStringParameter("StorageDirectory", "OrthancStorage"); - ServerIndex index(storageDirectory); - MyDicomStoreFactory storeScp(index, storageDirectory); + boost::filesystem::path storageDirectory = GetGlobalStringParameter("StorageDirectory", "OrthancStorage"); + 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); { // DICOM server @@ -146,7 +245,7 @@ // HTTP server MongooseServer httpServer; - httpServer.SetPort(GetGlobalIntegerParameter("HttpPort", 8000)); + httpServer.SetPortNumber(GetGlobalIntegerParameter("HttpPort", 8042)); httpServer.SetRemoteAccessAllowed(GetGlobalBoolParameter("RemoteAccessAllowed", false)); httpServer.SetAuthenticationEnabled(GetGlobalBoolParameter("AuthenticationEnabled", false)); @@ -163,23 +262,26 @@ httpServer.SetSslEnabled(false); } + LOG(WARNING) << "DICOM server listening on port: " << dicomServer.GetPortNumber(); + LOG(WARNING) << "HTTP server listening on port: " << httpServer.GetPortNumber(); + #if ORTHANC_STANDALONE == 1 httpServer.RegisterHandler(new EmbeddedResourceHttpHandler("/app", EmbeddedResources::ORTHANC_EXPLORER)); #else httpServer.RegisterHandler(new FilesystemHttpHandler("/app", ORTHANC_PATH "/OrthancExplorer")); #endif - httpServer.RegisterHandler(new OrthancRestApi(index, storageDirectory)); + httpServer.RegisterHandler(new OrthancRestApi(context)); // GO !!! httpServer.Start(); dicomServer.Start(); - LOG(INFO) << "The server has started"; + LOG(WARNING) << "Orthanc has started"; Toolbox::ServerBarrier(); // Stop - LOG(INFO) << "The server is stopping"; + LOG(WARNING) << "Orthanc is stopping"; } storeScp.Done(); @@ -187,6 +289,7 @@ catch (OrthancException& e) { LOG(ERROR) << "EXCEPTION [" << e.What() << "]"; +return -1; } OrthancFinalize();
--- a/README Thu Oct 04 15:09:56 2012 +0200 +++ b/README Fri Dec 14 11:24:24 2012 +0100 @@ -37,10 +37,13 @@ Licensing --------- -Orthanc is licensed under the GPLv3 license. Because Orthanc uses the -Software-as-a-Service paradigm, commercial products are allowed to -access the Orthanc REST services and to bundle Orthanc in an -commercial aggregate. +Orthanc is licensed under the GPLv3 license, with the OpenSSL +exception: +http://people.gnome.org/~markmc/openssl-and-the-gpl.html + +Because Orthanc uses the Software-as-a-Service paradigm, commercial +products are allowed to access the Orthanc REST services and to bundle +Orthanc in an commercial aggregate. We also kindly require scientific works and clinical studies that make use of Orthanc to cite Orthanc in their associated publications.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Archives/MessageWithDestination.cpp Fri Dec 14 11:24:24 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"); +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Archives/PrepareDatabase-v1.sql Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,136 @@ +CREATE TABLE GlobalProperties( + name TEXT PRIMARY KEY, + value TEXT + ); + +CREATE TABLE Resources( + uuid TEXT PRIMARY KEY, + resourceType INTEGER + ); + +CREATE TABLE Patients( + uuid TEXT PRIMARY KEY, + dicomPatientId TEXT + ); + +CREATE TABLE Studies( + uuid TEXT PRIMARY KEY, + parentPatient TEXT REFERENCES Patients(uuid) ON DELETE CASCADE, + dicomStudy TEXT + ); + +CREATE TABLE Series( + uuid TEXT PRIMARY KEY, + parentStudy TEXT REFERENCES Studies(uuid) ON DELETE CASCADE, + dicomSeries TEXT, + expectedNumberOfInstances INTEGER + ); + +CREATE TABLE Instances( + uuid TEXT PRIMARY KEY, + parentSeries TEXT REFERENCES Series(uuid) ON DELETE CASCADE, + dicomInstance TEXT, + fileUuid TEXT, + fileSize INTEGER, + jsonUuid TEXT, + distantAet TEXT, + indexInSeries INTEGER + ); + +CREATE TABLE MainDicomTags( + uuid TEXT, + tagGroup INTEGER, + tagElement INTEGER, + value TEXT, + PRIMARY KEY(uuid, tagGroup, tagElement) + ); + +CREATE TABLE Changes( + seq INTEGER PRIMARY KEY AUTOINCREMENT, + basePath TEXT, + uuid TEXT + ); + + +CREATE INDEX PatientToStudies ON Studies(parentPatient); +CREATE INDEX StudyToSeries ON Series(parentStudy); +CREATE INDEX SeriesToInstances ON Instances(parentSeries); + +CREATE INDEX DicomPatientIndex ON Patients(dicomPatientId); +CREATE INDEX DicomStudyIndex ON Studies(dicomStudy); +CREATE INDEX DicomSeriesIndex ON Series(dicomSeries); +CREATE INDEX DicomInstanceIndex ON Instances(dicomInstance); + +CREATE INDEX MainDicomTagsIndex ON MainDicomTags(uuid); +CREATE INDEX MainDicomTagsGroupElement ON MainDicomTags(tagGroup, tagElement); +CREATE INDEX MainDicomTagsValues ON MainDicomTags(value COLLATE BINARY); + +CREATE INDEX ChangesIndex ON Changes(uuid); + +CREATE TRIGGER InstanceRemoved +AFTER DELETE ON Instances +FOR EACH ROW BEGIN + DELETE FROM Resources WHERE uuid = old.uuid; + DELETE FROM MainDicomTags WHERE uuid = old.uuid; + DELETE FROM Changes WHERE uuid = old.uuid; + SELECT DeleteFromFileStorage(old.fileUuid); + SELECT DeleteFromFileStorage(old.jsonUuid); + SELECT SignalDeletedLevel(3, old.parentSeries); +END; + +CREATE TRIGGER SeriesRemoved +AFTER DELETE ON Series +FOR EACH ROW BEGIN + DELETE FROM Resources WHERE uuid = old.uuid; + DELETE FROM MainDicomTags WHERE uuid = old.uuid; + DELETE FROM Changes WHERE uuid = old.uuid; + SELECT SignalDeletedLevel(2, old.parentStudy); +END; + +CREATE TRIGGER StudyRemoved +AFTER DELETE ON Studies +FOR EACH ROW BEGIN + DELETE FROM Resources WHERE uuid = old.uuid; + DELETE FROM MainDicomTags WHERE uuid = old.uuid; + DELETE FROM Changes WHERE uuid = old.uuid; + SELECT SignalDeletedLevel(1, old.parentPatient); +END; + +CREATE TRIGGER PatientRemoved +AFTER DELETE ON Patients +FOR EACH ROW BEGIN + DELETE FROM Resources WHERE uuid = old.uuid; + DELETE FROM MainDicomTags WHERE uuid = old.uuid; + DELETE FROM Changes WHERE uuid = old.uuid; + SELECT SignalDeletedLevel(0, ""); +END; + + + + +CREATE TRIGGER InstanceRemovedUpwardCleaning +AFTER DELETE ON Instances +FOR EACH ROW + WHEN (SELECT COUNT(*) FROM Instances WHERE parentSeries = old.parentSeries) = 0 + BEGIN + SELECT DeleteFromFileStorage("deleting parent series"); -- TODO REMOVE THIS + DELETE FROM Series WHERE uuid = old.parentSeries; + END; + +CREATE TRIGGER SeriesRemovedUpwardCleaning +AFTER DELETE ON Series +FOR EACH ROW + WHEN (SELECT COUNT(*) FROM Series WHERE parentStudy = old.parentStudy) = 0 + BEGIN + SELECT DeleteFromFileStorage("deleting parent study"); -- TODO REMOVE THIS + DELETE FROM Studies WHERE uuid = old.parentStudy; + END; + +CREATE TRIGGER StudyRemovedUpwardCleaning +AFTER DELETE ON Studies +FOR EACH ROW + WHEN (SELECT COUNT(*) FROM Studies WHERE parentPatient = old.parentPatient) = 0 + BEGIN + SELECT DeleteFromFileStorage("deleting parent patient"); -- TODO REMOVE THIS + DELETE FROM Patients WHERE uuid = old.parentPatient; + END;
--- a/Resources/CMake/BoostConfiguration.cmake Thu Oct 04 15:09:56 2012 +0200 +++ b/Resources/CMake/BoostConfiguration.cmake Fri Dec 14 11:24:24 2012 +0100 @@ -8,7 +8,7 @@ #set(Boost_USE_STATIC_LIBS ON) find_package(Boost - COMPONENTS filesystem thread system) + COMPONENTS filesystem thread system date_time) if (NOT Boost_FOUND) message(FATAL_ERROR "Unable to locate Boost on this system") @@ -52,6 +52,11 @@ add_definitions( -DBOOST_LOCALE_WITH_ICONV=1 ) + + if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") + add_definitions(-DBOOST_HAS_SCHED_YIELD=1) + endif() + elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Windows") list(APPEND BOOST_SOURCES ${BOOST_SOURCES_DIR}/libs/thread/src/win32/tss_dll.cpp
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/CMake/Compiler.cmake Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,94 @@ +# This file sets all the compiler-related flags + +if (${CMAKE_COMPILER_IS_GNUCXX}) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wno-long-long -Wno-implicit-function-declaration") + # --std=c99 makes libcurl not to compile + # -pedantic gives a lot of warnings on OpenSSL + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -pedantic -Wno-long-long -Wno-variadic-macros") +elseif (${MSVC}) + # http://stackoverflow.com/a/6510446 + foreach(flag_var + CMAKE_C_FLAGS_DEBUG + CMAKE_CXX_FLAGS_DEBUG + CMAKE_C_FLAGS_RELEASE + CMAKE_CXX_FLAGS_RELEASE + CMAKE_C_FLAGS_MINSIZEREL + CMAKE_CXX_FLAGS_MINSIZEREL + CMAKE_C_FLAGS_RELWITHDEBINFO + CMAKE_CXX_FLAGS_RELWITHDEBINFO) + string(REGEX REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}") + string(REGEX REPLACE "/MDd" "/MTd" ${flag_var} "${${flag_var}}") + endforeach(flag_var) + add_definitions( + -D_CRT_SECURE_NO_WARNINGS=1 + -D_CRT_SECURE_NO_DEPRECATE=1 + ) + include_directories(${CMAKE_SOURCE_DIR}/Resources/VisualStudio) + link_libraries(netapi32) +endif() + + +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() + + add_definitions( + -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}") + + # Remove the "-rdynamic" option + # http://www.mail-archive.com/cmake@cmake.org/msg08837.html + set(CMAKE_SHARED_LIBRARY_LINK_CXX_FLAGS "") + link_libraries(uuid pthread rt) + +elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + add_definitions( + -DWINVER=0x0501 + -D_CRT_SECURE_NO_WARNINGS=1 + ) + link_libraries(rpcrt4 ws2_32) +endif() + + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + CHECK_INCLUDE_FILES(rpc.h HAVE_UUID_H) +else() + CHECK_INCLUDE_FILES(uuid/uuid.h HAVE_UUID_H) +endif() + +if (NOT HAVE_UUID_H) + message(FATAL_ERROR "Please install the uuid-dev package") +endif() + +if (${STATIC_BUILD}) + add_definitions(-DORTHANC_STATIC=1) +else() + add_definitions(-DORTHANC_STATIC=0) +endif()
--- a/Resources/CMake/DcmtkConfiguration.cmake Thu Oct 04 15:09:56 2012 +0200 +++ b/Resources/CMake/DcmtkConfiguration.cmake Fri Dec 14 11:24:24 2012 +0100 @@ -1,6 +1,3 @@ -# We always statically link against DCMTK 3.6.0, as there are many -# differences wrt. DCMTK 3.5.x. - if (${STATIC_BUILD}) SET(DCMTK_VERSION_NUMBER 360) SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.0) @@ -12,27 +9,42 @@ SET(DCMTK_SOURCE_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.0) include(${DCMTK_SOURCES_DIR}/CMake/CheckFunctionWithHeaderExists.cmake) include(${DCMTK_SOURCES_DIR}/CMake/GenerateDCMTKConfigure.cmake) - CONFIGURE_FILE(${DCMTK_SOURCES_DIR}/CMake/osconfig.h.in + + if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") + set(HAVE_SSTREAM 1) + set(HAVE_PROTOTYPE_BZERO 1) + set(HAVE_PROTOTYPE_GETHOSTNAME 1) + set(HAVE_PROTOTYPE_GETSOCKOPT 1) + set(HAVE_PROTOTYPE_SETSOCKOPT 1) + set(HAVE_PROTOTYPE_CONNECT 1) + set(HAVE_PROTOTYPE_BIND 1) + set(HAVE_PROTOTYPE_ACCEPT 1) + set(HAVE_PROTOTYPE_SETSOCKNAME 1) + set(HAVE_PROTOTYPE_GETSOCKNAME 1) + endif() + + CONFIGURE_FILE( + ${DCMTK_SOURCES_DIR}/CMake/osconfig.h.in ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/osconfig.h) - AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmnet/libsrc THIRD_PARTY_SOURCES) - AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmdata/libsrc THIRD_PARTY_SOURCES) - AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/ofstd/libsrc THIRD_PARTY_SOURCES) + AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmnet/libsrc DCMTK_SOURCES) + AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmdata/libsrc DCMTK_SOURCES) + AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/ofstd/libsrc DCMTK_SOURCES) # Source for the logging facility of DCMTK - AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/oflog/libsrc THIRD_PARTY_SOURCES) + AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/oflog/libsrc DCMTK_SOURCES) if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") - list(REMOVE_ITEM THIRD_PARTY_SOURCES + list(REMOVE_ITEM DCMTK_SOURCES ${DCMTK_SOURCES_DIR}/oflog/libsrc/windebap.cc ${DCMTK_SOURCES_DIR}/oflog/libsrc/winsock.cc ) elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") - list(REMOVE_ITEM THIRD_PARTY_SOURCES + list(REMOVE_ITEM DCMTK_SOURCES ${DCMTK_SOURCES_DIR}/oflog/libsrc/unixsock.cc ) endif() - list(REMOVE_ITEM THIRD_PARTY_SOURCES + list(REMOVE_ITEM DCMTK_SOURCES ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/mkdictbi.cc ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/mkdeftag.cc ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/dcdictbi.cc @@ -59,37 +71,39 @@ 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 ) - if (NOT EXISTS "${DCMTK_DIR}/config/cfunix.h") + if (EXISTS "${DCMTK_DIR}/config/cfunix.h") + set(DCMTK_CONFIGURATION_FILE "${DCMTK_DIR}/config/cfunix.h") + elseif (EXISTS "${DCMTK_DIR}/config/osconfig.h") # This is for Arch Linux + set(DCMTK_CONFIGURATION_FILE "${DCMTK_DIR}/config/osconfig.h") + else() message(FATAL_ERROR "Please install libdcmtk1-dev") endif() # Autodetection of the version of DCMTK - file(STRINGS "${DCMTK_DIR}/config/cfunix.h" 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/CMake/GoogleLogConfiguration.cmake Thu Oct 04 15:09:56 2012 +0200 +++ b/Resources/CMake/GoogleLogConfiguration.cmake Fri Dec 14 11:24:24 2012 +0100 @@ -1,7 +1,46 @@ -if (${STATIC_BUILD}) +if (STATIC_BUILD OR NOT USE_DYNAMIC_GOOGLE_LOG) SET(GOOGLE_LOG_SOURCES_DIR ${CMAKE_BINARY_DIR}/glog-0.3.2) DownloadPackage("http://google-glog.googlecode.com/files/glog-0.3.2.tar.gz" "${GOOGLE_LOG_SOURCES_DIR}" "" "") + set(GOOGLE_LOG_HEADERS + ${GOOGLE_LOG_SOURCES_DIR}/src/glog/logging.h + ${GOOGLE_LOG_SOURCES_DIR}/src/glog/raw_logging.h + ${GOOGLE_LOG_SOURCES_DIR}/src/glog/stl_logging.h + ${GOOGLE_LOG_SOURCES_DIR}/src/glog/vlog_is_on.h + ) + + set(ac_google_namespace google) + set(ac_google_start_namespace "namespace google {") + set(ac_google_end_namespace "}") + + if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + set(ac_cv_have_unistd_h 1) + set(ac_cv_have_stdint_h 1) + set(ac_cv_have_systypes_h 0) + set(ac_cv_have_inttypes_h 0) + set(ac_cv_have_libgflags 0) + set(ac_cv_have_uint16_t 1) + set(ac_cv_have_u_int16_t 0) + set(ac_cv_have___uint16 0) + set(ac_cv_cxx_using_operator 1) + set(ac_cv_have___builtin_expect 1) + else() + set(ac_cv_have_unistd_h 0) + set(ac_cv_have_stdint_h 0) + set(ac_cv_have_systypes_h 0) + set(ac_cv_have_inttypes_h 0) + set(ac_cv_have_libgflags 0) + set(ac_cv_have_uint16_t 0) + set(ac_cv_have_u_int16_t 0) + set(ac_cv_have___uint16 1) + set(ac_cv_cxx_using_operator 1) + set(ac_cv_have___builtin_expect 0) + endif() + + foreach (f ${GOOGLE_LOG_HEADERS}) + configure_file(${f}.in ${f}) + endforeach() + include_directories( ${GOOGLE_LOG_SOURCES_DIR}/src )
--- a/Resources/CMake/GoogleLogConfiguration.h Thu Oct 04 15:09:56 2012 +0200 +++ b/Resources/CMake/GoogleLogConfiguration.h Fri Dec 14 11:24:24 2012 +0100 @@ -138,7 +138,16 @@ #define PACKAGE_VERSION "0.3.2" /* How to access the PC from a struct ucontext */ +/*#include <ucontext.h> +#include <sys/ucontext.h> +#ifdef REG_RIP #define PC_FROM_UCONTEXT uc_mcontext.gregs[REG_RIP] +#else +#undef PC_FROM_UCONTEXT +#endif*/ + +// This is required for older versions of Linux +#undef PC_FROM_UCONTEXT /* Define to necessary symbol if this constant uses a non-standard name on your system. */
--- a/Resources/CMake/GoogleTestConfiguration.cmake Thu Oct 04 15:09:56 2012 +0200 +++ b/Resources/CMake/GoogleTestConfiguration.cmake Fri Dec 14 11:24:24 2012 +0100 @@ -1,4 +1,13 @@ -if (${STATIC_BUILD}) +if (DEBIAN_USE_GTEST_SOURCE_PACKAGE) + set(GTEST_SOURCES /usr/src/gtest/src/gtest-all.cc) + include_directories(/usr/src/gtest) + + if (NOT EXISTS /usr/include/gtest/gtest.h OR + NOT EXISTS ${GTEST_SOURCES}) + message(FATAL_ERROR "Please install the libgtest-dev package") + endif() + +elseif (STATIC_BUILD OR NOT USE_DYNAMIC_GOOGLE_TEST) SET(GTEST_SOURCES_DIR ${CMAKE_BINARY_DIR}/gtest-1.6.0) DownloadPackage("http://googletest.googlecode.com/files/gtest-1.6.0.zip" "${GTEST_SOURCES_DIR}" "" "") @@ -13,6 +22,10 @@ else() include(FindGTest) + if (NOT GTEST_FOUND) + message(FATAL_ERROR "Unable to find GoogleTest") + endif() + include_directories(${GTEST_INCLUDE_DIRS}) link_libraries(${GTEST_LIBRARIES}) endif()
--- a/Resources/CMake/JsonCppConfiguration.cmake Thu Oct 04 15:09:56 2012 +0200 +++ b/Resources/CMake/JsonCppConfiguration.cmake Fri Dec 14 11:24:24 2012 +0100 @@ -1,14 +1,26 @@ -SET(JSONCPP_SOURCES_DIR ${CMAKE_BINARY_DIR}/jsoncpp-src-0.5.0) -DownloadPackage("http://downloads.sourceforge.net/project/jsoncpp/jsoncpp/0.5.0/jsoncpp-src-0.5.0.tar.gz" "${JSONCPP_SOURCES_DIR}" "" "") +if (USE_DYNAMIC_JSONCPP) + CHECK_INCLUDE_FILE_CXX(jsoncpp/json/reader.h HAVE_JSONCPP_H) + if (NOT HAVE_JSONCPP_H) + message(FATAL_ERROR "Please install the libjsoncpp-dev package") + endif() + + include_directories(/usr/include/jsoncpp) + link_libraries(jsoncpp) + +else() + SET(JSONCPP_SOURCES_DIR ${CMAKE_BINARY_DIR}/jsoncpp-src-0.5.0) + DownloadPackage("http://downloads.sourceforge.net/project/jsoncpp/jsoncpp/0.5.0/jsoncpp-src-0.5.0.tar.gz" "${JSONCPP_SOURCES_DIR}" "" "") -list(APPEND THIRD_PARTY_SOURCES - ${JSONCPP_SOURCES_DIR}/src/lib_json/json_reader.cpp - ${JSONCPP_SOURCES_DIR}/src/lib_json/json_value.cpp - ${JSONCPP_SOURCES_DIR}/src/lib_json/json_writer.cpp - ) + list(APPEND THIRD_PARTY_SOURCES + ${JSONCPP_SOURCES_DIR}/src/lib_json/json_reader.cpp + ${JSONCPP_SOURCES_DIR}/src/lib_json/json_value.cpp + ${JSONCPP_SOURCES_DIR}/src/lib_json/json_writer.cpp + ) -include_directories( - ${JSONCPP_SOURCES_DIR}/include - ) + include_directories( + ${JSONCPP_SOURCES_DIR}/include + ) -source_group(ThirdParty\\JsonCpp REGULAR_EXPRESSION ${JSONCPP_SOURCES_DIR}/.*) + source_group(ThirdParty\\JsonCpp REGULAR_EXPRESSION ${JSONCPP_SOURCES_DIR}/.*) +endif() +
--- a/Resources/CMake/SQLiteConfiguration.cmake Thu Oct 04 15:09:56 2012 +0200 +++ b/Resources/CMake/SQLiteConfiguration.cmake Fri Dec 14 11:24:24 2012 +0100 @@ -1,19 +1,39 @@ -SET(SQLITE_SOURCES_DIR ${CMAKE_BINARY_DIR}/sqlite-amalgamation-3071300) -DownloadPackage("http://www.sqlite.org/sqlite-amalgamation-3071300.zip" "${SQLITE_SOURCES_DIR}" "" "") +if (STATIC_BUILD OR NOT USE_DYNAMIC_SQLITE) + SET(SQLITE_SOURCES_DIR ${CMAKE_BINARY_DIR}/sqlite-amalgamation-3071300) + DownloadPackage("http://www.sqlite.org/sqlite-amalgamation-3071300.zip" "${SQLITE_SOURCES_DIR}" "" "") + + list(APPEND THIRD_PARTY_SOURCES + ${SQLITE_SOURCES_DIR}/sqlite3.c + ) -list(APPEND THIRD_PARTY_SOURCES - ${SQLITE_SOURCES_DIR}/sqlite3.c - ) + add_definitions( + # For SQLite to run in the "Serialized" thread-safe mode + # http://www.sqlite.org/threadsafe.html + -DSQLITE_THREADSAFE=1 + -DSQLITE_OMIT_LOAD_EXTENSION # Disable SQLite plugins + ) + + include_directories( + ${SQLITE_SOURCES_DIR} + ) -add_definitions( - # For SQLite to run in the "Serialized" thread-safe mode - # http://www.sqlite.org/threadsafe.html - -DSQLITE_THREADSAFE=1 - -DSQLITE_OMIT_LOAD_EXTENSION # Disable SQLite plugins - ) + source_group(ThirdParty\\SQLite REGULAR_EXPRESSION ${SQLITE_SOURCES_DIR}/.*) +else() + CHECK_INCLUDE_FILE_CXX(sqlite3.h HAVE_SQLITE_H) + if (NOT HAVE_SQLITE_H) + message(FATAL_ERROR "Please install the libsqlite3-dev package") + endif() -include_directories( - ${SQLITE_SOURCES_DIR} - ) + # Autodetection of the version of SQLite + file(STRINGS "/usr/include/sqlite3.h" SQLITE_VERSION_NUMBER1 REGEX "#define SQLITE_VERSION_NUMBER.*$") + string(REGEX REPLACE "#define SQLITE_VERSION_NUMBER(.*)$" "\\1" SQLITE_VERSION_NUMBER ${SQLITE_VERSION_NUMBER1}) + + message("Detected version of SQLite: ${SQLITE_VERSION_NUMBER}") -source_group(ThirdParty\\SQLite REGULAR_EXPRESSION ${SQLITE_SOURCES_DIR}/.*) \ No newline at end of file + IF (${SQLITE_VERSION_NUMBER} LESS 3007000) + # "sqlite3_create_function_v2" is not defined in SQLite < 3.7.0 + message(FATAL_ERROR "SQLite version must be above 3.7.0. Please set the CMake variable USE_DYNAMIC_SQLITE to OFF.") + ENDIF() + + link_libraries(sqlite3) +endif()
--- a/Resources/Configuration.json Thu Oct 04 15:09:56 2012 +0200 +++ b/Resources/Configuration.json Fri Dec 14 11:24:24 2012 +0100 @@ -3,9 +3,24 @@ * General configuration of Orthanc **/ + // The logical name of this instance of Orthanc. This one is + // displayed in Orthanc Explorer and at the URI "/system". + "Name" : "MyOrthanc", + // Path to the directory that holds the database "StorageDirectory" : "OrthancStorage", + // 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, /** @@ -13,7 +28,7 @@ **/ // HTTP port for the REST services and for the GUI - "HttpPort" : 8000, + "HttpPort" : 8042, @@ -37,7 +52,7 @@ **/ // Whether remote hosts can connect to the HTTP server - "RemoteAccessAllowed" : true, + "RemoteAccessAllowed" : false, // Whether or not SSL is enabled "SslEnabled" : false, @@ -62,7 +77,12 @@ // The list of the known DICOM modalities "DicomModalities" : { - // "sample" : [ "SAMPLESCP", "192.168.100.42", 104 ] + /** + * Uncommenting the following line would enable Orthanc to + * connect to an instance of the "storescp" open-source DICOM + * store started by the command line "storescp 2000". + **/ + // "sample" : [ "STORESCP", "localhost", 2000 ] }, // The list of the known Orthanc peers (currently unused)
--- a/Resources/EmbedResources.py Thu Oct 04 15:09:56 2012 +0200 +++ b/Resources/EmbedResources.py Fri Dec 14 11:24:24 2012 +0100 @@ -1,7 +1,38 @@ +# 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/>. + + import sys import os import os.path import pprint +import re if len(sys.argv) < 2 or len(sys.argv) % 2 != 0: print ('Usage:') @@ -20,6 +51,10 @@ ## Read each resource file ##################################################################### +def CheckNoUpcase(s): + if re.search('[A-Z]', s) != None: + raise Exception("Path in a directory with an upcase letter: %s" % s) + resources = {} counter = 0 @@ -42,10 +77,11 @@ for f in files: if f.find('~') == -1: # Ignore Emacs backup files if base == '.': - r = f.lower() + r = f else: - r = os.path.join(base, f).lower() + r = os.path.join(base, f) + CheckNoUpcase(r) r = '/' + r.replace('\\', '/') if r in content: raise Exception("Twice the same filename (check case): " + r)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/LinuxStandardBaseToolchain.cmake Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,41 @@ +INCLUDE(CMakeForceCompiler) + +SET(LSB_PATH $ENV{LSB_PATH}) +SET(LSB_TARGET_VERSION "4.0") + +IF ("${LSB_PATH}" STREQUAL "") + SET(LSB_PATH "/opt/lsb") +ENDIF() + +message("Using the following Linux Standard Base: ${LSB_PATH}") + +IF (EXISTS ${LSB_PATH}/lib64) + SET(LSB_TARGET_PROCESSOR "x86_64") +ELSEIF (EXISTS ${LSB_PATH}/lib) + SET(LSB_TARGET_PROCESSOR "x86") +ELSE() + MESSAGE(FATAL_ERROR "Unable to detect the target processor architecture. Check the LSB_PATH environment variable.") +ENDIF() + +SET(LSB_CPPPATH ${LSB_PATH}/include) +SET(LSB_LIBPATH ${LSB_PATH}/lib-${LSB_TARGET_VERSION}) +SET(PKG_CONFIG_PATH ${LSB_LIBPATH}/pkgconfig/) + +# the name of the target operating system +SET(CMAKE_SYSTEM_NAME Linux) +SET(CMAKE_SYSTEM_VERSION LinuxStandardBase) +SET(CMAKE_SYSTEM_PROCESSOR ${LSB_TARGET_PROCESSOR}) + +# which compilers to use for C and C++ +SET(CMAKE_C_COMPILER ${LSB_PATH}/bin/lsbcc) +CMAKE_FORCE_CXX_COMPILER(${LSB_PATH}/bin/lsbc++ GNU) + +# here is the target environment located +SET(CMAKE_FIND_ROOT_PATH ${LSB_PATH}) + +# adjust the default behaviour of the FIND_XXX() commands: +# search headers and libraries in the target environment, search +# programs in the host environment +SET(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +SET(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +SET(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Samples/ImportDicomFiles/ImportDicomFiles.py Fri Dec 14 11:24:24 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:24:24 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:24:24 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/Resources/sha1/Makefile Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,41 @@ +# +# Makefile +# +# Copyright (C) 1998, 2009 +# Paul E. Jones <paulej@packetizer.com> +# All Rights Reserved. +# +############################################################################# +# $Id: Makefile 12 2009-06-22 19:34:25Z paulej $ +############################################################################# +# +# Description: +# This is a makefile for UNIX to build the programs sha, shacmp, and +# shatest +# +# + +CC = g++ + +CFLAGS = -c -O2 -Wall -D_FILE_OFFSET_BITS=64 + +LIBS = + +OBJS = sha1.o + +all: sha shacmp shatest + +sha: sha.o $(OBJS) + $(CC) -o $@ sha.o $(OBJS) $(LIBS) + +shacmp: shacmp.o $(OBJS) + $(CC) -o $@ shacmp.o $(OBJS) $(LIBS) + +shatest: shatest.o $(OBJS) + $(CC) -o $@ shatest.o $(OBJS) $(LIBS) + +%.o: %.cpp + $(CC) $(CFLAGS) -o $@ $< + +clean: + $(RM) *.o sha shacmp shatest
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/sha1/Makefile.nt Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,48 @@ +# +# Makefile.nt +# +# Copyright (C) 1998, 2009 +# Paul E. Jones <paulej@packetizer.com> +# All Rights Reserved. +# +############################################################################# +# $Id: Makefile.nt 13 2009-06-22 20:20:32Z paulej $ +############################################################################# +# +# Description: +# This is a makefile for Win32 to build the programs sha, shacmp, and +# shatest +# +# Portability Issues: +# Designed to work with Visual C++ +# +# + +.silent: + +!include <win32.mak> + +RM = del /q + +LIBS = $(conlibs) setargv.obj + +CFLAGS = -D _CRT_SECURE_NO_WARNINGS /EHsc /O2 /W3 + +OBJS = sha1.obj + +all: sha.exe shacmp.exe shatest.exe + +sha.exe: sha.obj $(OBJS) + $(link) $(conflags) -out:$@ sha.obj $(OBJS) $(LIBS) + +shacmp.exe: shacmp.obj $(OBJS) + $(link) $(conflags) -out:$@ shacmp.obj $(OBJS) $(LIBS) + +shatest.exe: shatest.obj $(OBJS) + $(link) $(conflags) -out:$@ shatest.obj $(OBJS) $(LIBS) + +.cpp.obj: + $(cc) $(CFLAGS) $(cflags) $(cvars) $< + +clean: + $(RM) *.obj sha.exe shacmp.exe shatest.exe
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/sha1/license.txt Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,14 @@ +Copyright (C) 1998, 2009 +Paul E. Jones <paulej@packetizer.com> + +Freeware Public License (FPL) + +This software is licensed as "freeware." Permission to distribute +this software in source and binary forms, including incorporation +into other products, is hereby granted without a fee. THIS SOFTWARE +IS PROVIDED 'AS IS' AND WITHOUT ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE. THE AUTHOR SHALL NOT BE HELD +LIABLE FOR ANY DAMAGES RESULTING FROM THE USE OF THIS SOFTWARE, EITHER +DIRECTLY OR INDIRECTLY, INCLUDING, BUT NOT LIMITED TO, LOSS OF DATA +OR DATA BEING RENDERED INACCURATE.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/sha1/sha.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,176 @@ +/* + * sha.cpp + * + * Copyright (C) 1998, 2009 + * Paul E. Jones <paulej@packetizer.com> + * All Rights Reserved + * + ***************************************************************************** + * $Id: sha.cpp 13 2009-06-22 20:20:32Z paulej $ + ***************************************************************************** + * + * Description: + * This utility will display the message digest (fingerprint) for + * the specified file(s). + * + * Portability Issues: + * None. + */ + +#include <stdio.h> +#include <string.h> +#ifdef WIN32 +#include <io.h> +#endif +#include <fcntl.h> +#include "sha1.h" + +/* + * Function prototype + */ +void usage(); + + +/* + * main + * + * Description: + * This is the entry point for the program + * + * Parameters: + * argc: [in] + * This is the count of arguments in the argv array + * argv: [in] + * This is an array of filenames for which to compute message digests + * + * Returns: + * Nothing. + * + * Comments: + * + */ +int main(int argc, char *argv[]) +{ + SHA1 sha; // SHA-1 class + FILE *fp; // File pointer for reading files + char c; // Character read from file + unsigned message_digest[5]; // Message digest from "sha" + int i; // Counter + bool reading_stdin; // Are we reading standard in? + bool read_stdin = false; // Have we read stdin? + + /* + * Check the program arguments and print usage information if -? + * or --help is passed as the first argument. + */ + if (argc > 1 && (!strcmp(argv[1],"-?") || !strcmp(argv[1],"--help"))) + { + usage(); + return 1; + } + + /* + * For each filename passed in on the command line, calculate the + * SHA-1 value and display it. + */ + for(i = 0; i < argc; i++) + { + /* + * We start the counter at 0 to guarantee entry into the for loop. + * So if 'i' is zero, we will increment it now. If there is no + * argv[1], we will use STDIN below. + */ + if (i == 0) + { + i++; + } + + if (argc == 1 || !strcmp(argv[i],"-")) + { +#ifdef WIN32 + _setmode(_fileno(stdin), _O_BINARY); +#endif + fp = stdin; + reading_stdin = true; + } + else + { + if (!(fp = fopen(argv[i],"rb"))) + { + fprintf(stderr, "sha: unable to open file %s\n", argv[i]); + return 2; + } + reading_stdin = false; + } + + /* + * We do not want to read STDIN multiple times + */ + if (reading_stdin) + { + if (read_stdin) + { + continue; + } + + read_stdin = true; + } + + /* + * Reset the SHA1 object and process input + */ + sha.Reset(); + + c = fgetc(fp); + while(!feof(fp)) + { + sha.Input(c); + c = fgetc(fp); + } + + if (!reading_stdin) + { + fclose(fp); + } + + if (!sha.Result(message_digest)) + { + fprintf(stderr,"sha: could not compute message digest for %s\n", + reading_stdin?"STDIN":argv[i]); + } + else + { + printf( "%08X %08X %08X %08X %08X - %s\n", + message_digest[0], + message_digest[1], + message_digest[2], + message_digest[3], + message_digest[4], + reading_stdin?"STDIN":argv[i]); + } + } + + return 0; +} + +/* + * usage + * + * Description: + * This function will display program usage information to the user. + * + * Parameters: + * None. + * + * Returns: + * Nothing. + * + * Comments: + * + */ +void usage() +{ + printf("usage: sha <file> [<file> ...]\n"); + printf("\tThis program will display the message digest (fingerprint)\n"); + printf("\tfor files using the Secure Hashing Algorithm (SHA-1).\n"); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/sha1/sha1.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,589 @@ +/* + * sha1.cpp + * + * Copyright (C) 1998, 2009 + * Paul E. Jones <paulej@packetizer.com> + * All Rights Reserved. + * + ***************************************************************************** + * $Id: sha1.cpp 12 2009-06-22 19:34:25Z paulej $ + ***************************************************************************** + * + * Description: + * This class implements the Secure Hashing Standard as defined + * in FIPS PUB 180-1 published April 17, 1995. + * + * The Secure Hashing Standard, which uses the Secure Hashing + * Algorithm (SHA), produces a 160-bit message digest for a + * given data stream. In theory, it is highly improbable that + * two messages will produce the same message digest. Therefore, + * this algorithm can serve as a means of providing a "fingerprint" + * for a message. + * + * Portability Issues: + * SHA-1 is defined in terms of 32-bit "words". This code was + * written with the expectation that the processor has at least + * a 32-bit machine word size. If the machine word size is larger, + * the code should still function properly. One caveat to that + * is that the input functions taking characters and character arrays + * assume that only 8 bits of information are stored in each character. + * + * Caveats: + * SHA-1 is designed to work with messages less than 2^64 bits long. + * Although SHA-1 allows a message digest to be generated for + * messages of any number of bits less than 2^64, this implementation + * only works with messages with a length that is a multiple of 8 + * bits. + * + */ + + +#include "sha1.h" + +/* + * SHA1 + * + * Description: + * This is the constructor for the sha1 class. + * + * Parameters: + * None. + * + * Returns: + * Nothing. + * + * Comments: + * + */ +SHA1::SHA1() +{ + Reset(); +} + +/* + * ~SHA1 + * + * Description: + * This is the destructor for the sha1 class + * + * Parameters: + * None. + * + * Returns: + * Nothing. + * + * Comments: + * + */ +SHA1::~SHA1() +{ + // The destructor does nothing +} + +/* + * Reset + * + * Description: + * This function will initialize the sha1 class member variables + * in preparation for computing a new message digest. + * + * Parameters: + * None. + * + * Returns: + * Nothing. + * + * Comments: + * + */ +void SHA1::Reset() +{ + Length_Low = 0; + Length_High = 0; + Message_Block_Index = 0; + + H[0] = 0x67452301; + H[1] = 0xEFCDAB89; + H[2] = 0x98BADCFE; + H[3] = 0x10325476; + H[4] = 0xC3D2E1F0; + + Computed = false; + Corrupted = false; +} + +/* + * Result + * + * Description: + * This function will return the 160-bit message digest into the + * array provided. + * + * Parameters: + * message_digest_array: [out] + * This is an array of five unsigned integers which will be filled + * with the message digest that has been computed. + * + * Returns: + * True if successful, false if it failed. + * + * Comments: + * + */ +bool SHA1::Result(unsigned *message_digest_array) +{ + int i; // Counter + + if (Corrupted) + { + return false; + } + + if (!Computed) + { + PadMessage(); + Computed = true; + } + + for(i = 0; i < 5; i++) + { + message_digest_array[i] = H[i]; + } + + return true; +} + +/* + * Input + * + * Description: + * This function accepts an array of octets as the next portion of + * the message. + * + * Parameters: + * message_array: [in] + * An array of characters representing the next portion of the + * message. + * + * Returns: + * Nothing. + * + * Comments: + * + */ +void SHA1::Input( const unsigned char *message_array, + unsigned length) +{ + if (!length) + { + return; + } + + if (Computed || Corrupted) + { + Corrupted = true; + return; + } + + while(length-- && !Corrupted) + { + Message_Block[Message_Block_Index++] = (*message_array & 0xFF); + + Length_Low += 8; + Length_Low &= 0xFFFFFFFF; // Force it to 32 bits + if (Length_Low == 0) + { + Length_High++; + Length_High &= 0xFFFFFFFF; // Force it to 32 bits + if (Length_High == 0) + { + Corrupted = true; // Message is too long + } + } + + if (Message_Block_Index == 64) + { + ProcessMessageBlock(); + } + + message_array++; + } +} + +/* + * Input + * + * Description: + * This function accepts an array of octets as the next portion of + * the message. + * + * Parameters: + * message_array: [in] + * An array of characters representing the next portion of the + * message. + * length: [in] + * The length of the message_array + * + * Returns: + * Nothing. + * + * Comments: + * + */ +void SHA1::Input( const char *message_array, + unsigned length) +{ + Input((unsigned char *) message_array, length); +} + +/* + * Input + * + * Description: + * This function accepts a single octets as the next message element. + * + * Parameters: + * message_element: [in] + * The next octet in the message. + * + * Returns: + * Nothing. + * + * Comments: + * + */ +void SHA1::Input(unsigned char message_element) +{ + Input(&message_element, 1); +} + +/* + * Input + * + * Description: + * This function accepts a single octet as the next message element. + * + * Parameters: + * message_element: [in] + * The next octet in the message. + * + * Returns: + * Nothing. + * + * Comments: + * + */ +void SHA1::Input(char message_element) +{ + Input((unsigned char *) &message_element, 1); +} + +/* + * operator<< + * + * Description: + * This operator makes it convenient to provide character strings to + * the SHA1 object for processing. + * + * Parameters: + * message_array: [in] + * The character array to take as input. + * + * Returns: + * A reference to the SHA1 object. + * + * Comments: + * Each character is assumed to hold 8 bits of information. + * + */ +SHA1& SHA1::operator<<(const char *message_array) +{ + const char *p = message_array; + + while(*p) + { + Input(*p); + p++; + } + + return *this; +} + +/* + * operator<< + * + * Description: + * This operator makes it convenient to provide character strings to + * the SHA1 object for processing. + * + * Parameters: + * message_array: [in] + * The character array to take as input. + * + * Returns: + * A reference to the SHA1 object. + * + * Comments: + * Each character is assumed to hold 8 bits of information. + * + */ +SHA1& SHA1::operator<<(const unsigned char *message_array) +{ + const unsigned char *p = message_array; + + while(*p) + { + Input(*p); + p++; + } + + return *this; +} + +/* + * operator<< + * + * Description: + * This function provides the next octet in the message. + * + * Parameters: + * message_element: [in] + * The next octet in the message + * + * Returns: + * A reference to the SHA1 object. + * + * Comments: + * The character is assumed to hold 8 bits of information. + * + */ +SHA1& SHA1::operator<<(const char message_element) +{ + Input((unsigned char *) &message_element, 1); + + return *this; +} + +/* + * operator<< + * + * Description: + * This function provides the next octet in the message. + * + * Parameters: + * message_element: [in] + * The next octet in the message + * + * Returns: + * A reference to the SHA1 object. + * + * Comments: + * The character is assumed to hold 8 bits of information. + * + */ +SHA1& SHA1::operator<<(const unsigned char message_element) +{ + Input(&message_element, 1); + + return *this; +} + +/* + * ProcessMessageBlock + * + * Description: + * This function will process the next 512 bits of the message + * stored in the Message_Block array. + * + * Parameters: + * None. + * + * Returns: + * Nothing. + * + * Comments: + * Many of the variable names in this function, especially the single + * character names, were used because those were the names used + * in the publication. + * + */ +void SHA1::ProcessMessageBlock() +{ + const unsigned K[] = { // Constants defined for SHA-1 + 0x5A827999, + 0x6ED9EBA1, + 0x8F1BBCDC, + 0xCA62C1D6 + }; + int t; // Loop counter + unsigned temp; // Temporary word value + unsigned W[80]; // Word sequence + unsigned A, B, C, D, E; // Word buffers + + /* + * Initialize the first 16 words in the array W + */ + for(t = 0; t < 16; t++) + { + W[t] = ((unsigned) Message_Block[t * 4]) << 24; + W[t] |= ((unsigned) Message_Block[t * 4 + 1]) << 16; + W[t] |= ((unsigned) Message_Block[t * 4 + 2]) << 8; + W[t] |= ((unsigned) Message_Block[t * 4 + 3]); + } + + for(t = 16; t < 80; t++) + { + W[t] = CircularShift(1,W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16]); + } + + A = H[0]; + B = H[1]; + C = H[2]; + D = H[3]; + E = H[4]; + + for(t = 0; t < 20; t++) + { + temp = CircularShift(5,A) + ((B & C) | ((~B) & D)) + E + W[t] + K[0]; + temp &= 0xFFFFFFFF; + E = D; + D = C; + C = CircularShift(30,B); + B = A; + A = temp; + } + + for(t = 20; t < 40; t++) + { + temp = CircularShift(5,A) + (B ^ C ^ D) + E + W[t] + K[1]; + temp &= 0xFFFFFFFF; + E = D; + D = C; + C = CircularShift(30,B); + B = A; + A = temp; + } + + for(t = 40; t < 60; t++) + { + temp = CircularShift(5,A) + + ((B & C) | (B & D) | (C & D)) + E + W[t] + K[2]; + temp &= 0xFFFFFFFF; + E = D; + D = C; + C = CircularShift(30,B); + B = A; + A = temp; + } + + for(t = 60; t < 80; t++) + { + temp = CircularShift(5,A) + (B ^ C ^ D) + E + W[t] + K[3]; + temp &= 0xFFFFFFFF; + E = D; + D = C; + C = CircularShift(30,B); + B = A; + A = temp; + } + + H[0] = (H[0] + A) & 0xFFFFFFFF; + H[1] = (H[1] + B) & 0xFFFFFFFF; + H[2] = (H[2] + C) & 0xFFFFFFFF; + H[3] = (H[3] + D) & 0xFFFFFFFF; + H[4] = (H[4] + E) & 0xFFFFFFFF; + + Message_Block_Index = 0; +} + +/* + * PadMessage + * + * Description: + * According to the standard, the message must be padded to an even + * 512 bits. The first padding bit must be a '1'. The last 64 bits + * represent the length of the original message. All bits in between + * should be 0. This function will pad the message according to those + * rules by filling the message_block array accordingly. It will also + * call ProcessMessageBlock() appropriately. When it returns, it + * can be assumed that the message digest has been computed. + * + * Parameters: + * None. + * + * Returns: + * Nothing. + * + * Comments: + * + */ +void SHA1::PadMessage() +{ + /* + * Check to see if the current message block is too small to hold + * the initial padding bits and length. If so, we will pad the + * block, process it, and then continue padding into a second block. + */ + if (Message_Block_Index > 55) + { + Message_Block[Message_Block_Index++] = 0x80; + while(Message_Block_Index < 64) + { + Message_Block[Message_Block_Index++] = 0; + } + + ProcessMessageBlock(); + + while(Message_Block_Index < 56) + { + Message_Block[Message_Block_Index++] = 0; + } + } + else + { + Message_Block[Message_Block_Index++] = 0x80; + while(Message_Block_Index < 56) + { + Message_Block[Message_Block_Index++] = 0; + } + + } + + /* + * Store the message length as the last 8 octets + */ + Message_Block[56] = (Length_High >> 24) & 0xFF; + Message_Block[57] = (Length_High >> 16) & 0xFF; + Message_Block[58] = (Length_High >> 8) & 0xFF; + Message_Block[59] = (Length_High) & 0xFF; + Message_Block[60] = (Length_Low >> 24) & 0xFF; + Message_Block[61] = (Length_Low >> 16) & 0xFF; + Message_Block[62] = (Length_Low >> 8) & 0xFF; + Message_Block[63] = (Length_Low) & 0xFF; + + ProcessMessageBlock(); +} + + +/* + * CircularShift + * + * Description: + * This member function will perform a circular shifting operation. + * + * Parameters: + * bits: [in] + * The number of bits to shift (1-31) + * word: [in] + * The value to shift (assumes a 32-bit integer) + * + * Returns: + * The shifted value. + * + * Comments: + * + */ +unsigned SHA1::CircularShift(int bits, unsigned word) +{ + return ((word << bits) & 0xFFFFFFFF) | ((word & 0xFFFFFFFF) >> (32-bits)); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/sha1/sha1.h Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,89 @@ +/* + * sha1.h + * + * Copyright (C) 1998, 2009 + * Paul E. Jones <paulej@packetizer.com> + * All Rights Reserved. + * + ***************************************************************************** + * $Id: sha1.h 12 2009-06-22 19:34:25Z paulej $ + ***************************************************************************** + * + * Description: + * This class implements the Secure Hashing Standard as defined + * in FIPS PUB 180-1 published April 17, 1995. + * + * Many of the variable names in this class, especially the single + * character names, were used because those were the names used + * in the publication. + * + * Please read the file sha1.cpp for more information. + * + */ + +#ifndef _SHA1_H_ +#define _SHA1_H_ + +class SHA1 +{ + + public: + + SHA1(); + virtual ~SHA1(); + + /* + * Re-initialize the class + */ + void Reset(); + + /* + * Returns the message digest + */ + bool Result(unsigned *message_digest_array); + + /* + * Provide input to SHA1 + */ + void Input( const unsigned char *message_array, + unsigned length); + void Input( const char *message_array, + unsigned length); + void Input(unsigned char message_element); + void Input(char message_element); + SHA1& operator<<(const char *message_array); + SHA1& operator<<(const unsigned char *message_array); + SHA1& operator<<(const char message_element); + SHA1& operator<<(const unsigned char message_element); + + private: + + /* + * Process the next 512 bits of the message + */ + void ProcessMessageBlock(); + + /* + * Pads the current message block to 512 bits + */ + void PadMessage(); + + /* + * Performs a circular left shift operation + */ + inline unsigned CircularShift(int bits, unsigned word); + + unsigned H[5]; // Message digest buffers + + unsigned Length_Low; // Message length in bits + unsigned Length_High; // Message length in bits + + unsigned char Message_Block[64]; // 512-bit message blocks + int Message_Block_Index; // Index into message block array + + bool Computed; // Is the digest computed? + bool Corrupted; // Is the message digest corruped? + +}; + +#endif
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/sha1/shacmp.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,169 @@ +/* + * shacmp.cpp + * + * Copyright (C) 1998, 2009 + * Paul E. Jones <paulej@packetizer.com> + * All Rights Reserved + * + ***************************************************************************** + * $Id: shacmp.cpp 12 2009-06-22 19:34:25Z paulej $ + ***************************************************************************** + * + * Description: + * This utility will compare two files by producing a message digest + * for each file using the Secure Hashing Algorithm and comparing + * the message digests. This function will return 0 if they + * compare or 1 if they do not or if there is an error. + * Errors result in a return code higher than 1. + * + * Portability Issues: + * none. + * + */ + +#include <stdio.h> +#include <string.h> +#include "sha1.h" + +/* + * Return codes + */ +#define SHA1_COMPARE 0 +#define SHA1_NO_COMPARE 1 +#define SHA1_USAGE_ERROR 2 +#define SHA1_FILE_ERROR 3 + +/* + * Function prototype + */ +void usage(); + +/* + * main + * + * Description: + * This is the entry point for the program + * + * Parameters: + * argc: [in] + * This is the count of arguments in the argv array + * argv: [in] + * This is an array of filenames for which to compute message digests + * + * Returns: + * Nothing. + * + * Comments: + * + */ +int main(int argc, char *argv[]) +{ + SHA1 sha; // SHA-1 class + FILE *fp; // File pointer for reading files + char c; // Character read from file + unsigned message_digest[2][5]; // Message digest for files + int i; // Counter + bool message_match; // Message digest match flag + int returncode; + + /* + * If we have two arguments, we will assume they are filenames. If + * we do not have to arguments, call usage() and exit. + */ + if (argc != 3) + { + usage(); + return SHA1_USAGE_ERROR; + } + + /* + * Get the message digests for each file + */ + for(i = 1; i <= 2; i++) + { + sha.Reset(); + + if (!(fp = fopen(argv[i],"rb"))) + { + fprintf(stderr, "sha: unable to open file %s\n", argv[i]); + return SHA1_FILE_ERROR; + } + + c = fgetc(fp); + while(!feof(fp)) + { + sha.Input(c); + c = fgetc(fp); + } + + fclose(fp); + + if (!sha.Result(message_digest[i-1])) + { + fprintf(stderr,"shacmp: could not compute message digest for %s\n", + argv[i]); + return SHA1_FILE_ERROR; + } + } + + /* + * Compare the message digest values + */ + message_match = true; + for(i = 0; i < 5; i++) + { + if (message_digest[0][i] != message_digest[1][i]) + { + message_match = false; + break; + } + } + + if (message_match) + { + printf("Fingerprints match:\n"); + returncode = SHA1_COMPARE; + } + else + { + printf("Fingerprints do not match:\n"); + returncode = SHA1_NO_COMPARE; + } + + printf( "\t%08X %08X %08X %08X %08X\n", + message_digest[0][0], + message_digest[0][1], + message_digest[0][2], + message_digest[0][3], + message_digest[0][4]); + printf( "\t%08X %08X %08X %08X %08X\n", + message_digest[1][0], + message_digest[1][1], + message_digest[1][2], + message_digest[1][3], + message_digest[1][4]); + + return returncode; +} + +/* + * usage + * + * Description: + * This function will display program usage information to the user. + * + * Parameters: + * None. + * + * Returns: + * Nothing. + * + * Comments: + * + */ +void usage() +{ + printf("usage: shacmp <file> <file>\n"); + printf("\tThis program will compare the message digests (fingerprints)\n"); + printf("\tfor two files using the Secure Hashing Algorithm (SHA-1).\n"); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/sha1/shatest.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,149 @@ +/* + * shatest.cpp + * + * Copyright (C) 1998, 2009 + * Paul E. Jones <paulej@packetizer.com> + * All Rights Reserved + * + ***************************************************************************** + * $Id: shatest.cpp 12 2009-06-22 19:34:25Z paulej $ + ***************************************************************************** + * + * Description: + * This file will exercise the SHA1 class and perform the three + * tests documented in FIPS PUB 180-1. + * + * Portability Issues: + * None. + * + */ + +#include <iostream> +#include "sha1.h" + +using namespace std; + +/* + * Define patterns for testing + */ +#define TESTA "abc" +#define TESTB "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq" + +/* + * Function prototype + */ +void DisplayMessageDigest(unsigned *message_digest); + +/* + * main + * + * Description: + * This is the entry point for the program + * + * Parameters: + * None. + * + * Returns: + * Nothing. + * + * Comments: + * + */ +int main() +{ + SHA1 sha; + unsigned message_digest[5]; + + /* + * Perform test A + */ + cout << endl << "Test A: 'abc'" << endl; + + sha.Reset(); + sha << TESTA; + + if (!sha.Result(message_digest)) + { + cerr << "ERROR-- could not compute message digest" << endl; + } + else + { + DisplayMessageDigest(message_digest); + cout << "Should match:" << endl; + cout << '\t' << "A9993E36 4706816A BA3E2571 7850C26C 9CD0D89D" << endl; + } + + /* + * Perform test B + */ + cout << endl << "Test B: " << TESTB << endl; + + sha.Reset(); + sha << TESTB; + + if (!sha.Result(message_digest)) + { + cerr << "ERROR-- could not compute message digest" << endl; + } + else + { + DisplayMessageDigest(message_digest); + cout << "Should match:" << endl; + cout << '\t' << "84983E44 1C3BD26E BAAE4AA1 F95129E5 E54670F1" << endl; + } + + /* + * Perform test C + */ + cout << endl << "Test C: One million 'a' characters" << endl; + + sha.Reset(); + for(int i = 1; i <= 1000000; i++) sha.Input('a'); + + if (!sha.Result(message_digest)) + { + cerr << "ERROR-- could not compute message digest" << endl; + } + else + { + DisplayMessageDigest(message_digest); + cout << "Should match:" << endl; + cout << '\t' << "34AA973C D4C4DAA4 F61EEB2B DBAD2731 6534016F" << endl; + } + + return 0; +} + +/* + * DisplayMessageDigest + * + * Description: + * Display Message Digest array + * + * Parameters: + * None. + * + * Returns: + * Nothing. + * + * Comments: + * + */ +void DisplayMessageDigest(unsigned *message_digest) +{ + ios::fmtflags flags; + + cout << '\t'; + + flags = cout.setf(ios::hex|ios::uppercase,ios::basefield); + cout.setf(ios::uppercase); + + for(int i = 0; i < 5 ; i++) + { + cout << message_digest[i] << ' '; + } + + cout << endl; + + cout.setf(flags); +}
--- a/THANKS Thu Oct 04 15:09:56 2012 +0200 +++ b/THANKS Fri Dec 14 11:24:24 2012 +0100 @@ -8,7 +8,26 @@ complete and exempt or errors. -Contributors ------------- +Code contributors +----------------- + +* Cyril Paulus (cyril.paulus@student.ulg.ac.be), for the build process + and suggestions about the REST API. + + +Artwork +------- + +https://code.google.com/p/orthanc/wiki/Logos -* Cyril Paulus (cyril.paulus@student.ulg.ac.be), for the build process. +* Benjamin Golinvaux (golinvauxb@gmail.com), for creating the official logo. +* Jean-François Degbomont (jfdegbo@gmail.com), for submitting a logo. +* Martin Jolly (martin.jolly@gmail.com), for submitting a logo. +* Philippe Sepers (sepers.philippe@gmail.com), for submitting a logo. + + +Debian +------ + +* Mathieu Malaterre (mathieu.malaterre@gmail.com), for sponsoring Orthanc. +* Andreas Tille (andreas@an3as.eu), for help about Debian packaging.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/UnitTests/FileStorage.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,149 @@ +#include "gtest/gtest.h" + +#include <ctype.h> +#include <glog/logging.h> + +#include "../Core/FileStorage/FileStorage.h" +#include "../OrthancServer/ServerIndex.h" +#include "../Core/Toolbox.h" +#include "../Core/OrthancException.h" +#include "../Core/Uuid.h" +#include "../Core/HttpServer/FilesystemHttpSender.h" +#include "../Core/HttpServer/BufferHttpSender.h" +#include "../Core/FileStorage/FileStorageAccessor.h" +#include "../Core/FileStorage/CompressedFileStorageAccessor.h" + +using namespace Orthanc; + +TEST(FileStorage, Basic) +{ + FileStorage s("FileStorageUnitTests"); + + std::string data = Toolbox::GenerateUuid(); + std::string uid = s.Create(data); + std::string d; + s.ReadFile(d, uid); + ASSERT_EQ(d.size(), data.size()); + ASSERT_FALSE(memcmp(&d[0], &data[0], data.size())); +} + +TEST(FileStorage, EndToEnd) +{ + FileStorage s("FileStorageUnitTests"); + s.Clear(); + + std::list<std::string> u; + for (unsigned int i = 0; i < 10; i++) + { + u.push_back(s.Create(Toolbox::GenerateUuid())); + } + + std::set<std::string> ss; + s.ListAllFiles(ss); + ASSERT_EQ(10u, ss.size()); + + unsigned int c = 0; + for (std::list<std::string>::iterator + i = u.begin(); i != u.end(); i++, c++) + { + ASSERT_TRUE(ss.find(*i) != ss.end()); + if (c < 5) + s.Remove(*i); + } + + s.ListAllFiles(ss); + ASSERT_EQ(5u, ss.size()); + + s.Clear(); + s.ListAllFiles(ss); + ASSERT_EQ(0u, ss.size()); +} + + +TEST(FileStorageAccessor, Simple) +{ + FileStorage s("FileStorageUnitTests"); + FileStorageAccessor accessor(s); + + std::string data = "Hello world"; + FileInfo info = accessor.Write(data, FileContentType_Dicom); + + std::string r; + accessor.Read(r, info.GetUuid()); + + ASSERT_EQ(data, r); + ASSERT_EQ(CompressionType_None, info.GetCompressionType()); + ASSERT_EQ(11u, info.GetUncompressedSize()); + ASSERT_EQ(11u, info.GetCompressedSize()); + ASSERT_EQ(FileContentType_Dicom, info.GetContentType()); +} + + +TEST(FileStorageAccessor, NoCompression) +{ + FileStorage s("FileStorageUnitTests"); + CompressedFileStorageAccessor accessor(s); + + accessor.SetCompressionForNextOperations(CompressionType_None); + std::string data = "Hello world"; + FileInfo info = accessor.Write(data, FileContentType_Dicom); + + std::string r; + accessor.Read(r, info.GetUuid()); + + ASSERT_EQ(data, r); + ASSERT_EQ(CompressionType_None, info.GetCompressionType()); + ASSERT_EQ(11u, info.GetUncompressedSize()); + ASSERT_EQ(11u, info.GetCompressedSize()); + ASSERT_EQ(FileContentType_Dicom, info.GetContentType()); +} + + +TEST(FileStorageAccessor, Compression) +{ + FileStorage s("FileStorageUnitTests"); + CompressedFileStorageAccessor accessor(s); + + accessor.SetCompressionForNextOperations(CompressionType_Zlib); + std::string data = "Hello world"; + FileInfo info = accessor.Write(data, FileContentType_Dicom); + + std::string r; + accessor.Read(r, info.GetUuid()); + + ASSERT_EQ(data, r); + ASSERT_EQ(CompressionType_Zlib, info.GetCompressionType()); + ASSERT_EQ(11u, info.GetUncompressedSize()); + ASSERT_EQ(FileContentType_Dicom, info.GetContentType()); +} + + +TEST(FileStorageAccessor, Mix) +{ + FileStorage s("FileStorageUnitTests"); + CompressedFileStorageAccessor accessor(s); + + std::string r; + std::string compressedData = "Hello"; + std::string uncompressedData = "HelloWorld"; + + accessor.SetCompressionForNextOperations(CompressionType_Zlib); + FileInfo compressedInfo = accessor.Write(compressedData, FileContentType_Dicom); + + accessor.SetCompressionForNextOperations(CompressionType_None); + FileInfo uncompressedInfo = accessor.Write(uncompressedData, FileContentType_Dicom); + + accessor.SetCompressionForNextOperations(CompressionType_Zlib); + accessor.Read(r, compressedInfo.GetUuid()); + ASSERT_EQ(compressedData, r); + + accessor.SetCompressionForNextOperations(CompressionType_None); + accessor.Read(r, compressedInfo.GetUuid()); + ASSERT_NE(compressedData, r); + + /* + // This test is too slow on Windows + accessor.SetCompressionForNextOperations(CompressionType_Zlib); + ASSERT_THROW(accessor.Read(r, uncompressedInfo.GetUuid()), OrthancException); + */ +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/UnitTests/MemoryCache.cpp Fri Dec 14 11:24:24 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_); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/UnitTests/RestApi.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,52 @@ +#include "gtest/gtest.h" + +#include <ctype.h> +#include <glog/logging.h> + +#include "../Core/RestApi/RestApi.h" +#include "../Core/Uuid.h" +#include "../Core/OrthancException.h" +#include "../Core/Compression/ZlibCompressor.h" + +using namespace Orthanc; + +TEST(RestApi, RestApiPath) +{ + RestApiPath::Components args; + UriComponents trail; + + { + RestApiPath uri("/coucou/{abc}/d/*"); + ASSERT_TRUE(uri.Match(args, trail, "/coucou/moi/d/e/f/g")); + ASSERT_EQ(1u, args.size()); + ASSERT_EQ(3u, trail.size()); + ASSERT_EQ("moi", args["abc"]); + ASSERT_EQ("e", trail[0]); + ASSERT_EQ("f", trail[1]); + ASSERT_EQ("g", trail[2]); + + ASSERT_FALSE(uri.Match(args, trail, "/coucou/moi/f")); + ASSERT_TRUE(uri.Match(args, trail, "/coucou/moi/d/")); + ASSERT_FALSE(uri.Match(args, trail, "/a/moi/d")); + ASSERT_FALSE(uri.Match(args, trail, "/coucou/moi")); + } + + { + RestApiPath uri("/coucou/{abc}/d"); + ASSERT_FALSE(uri.Match(args, trail, "/coucou/moi/d/e/f/g")); + ASSERT_TRUE(uri.Match(args, trail, "/coucou/moi/d")); + ASSERT_EQ(1u, args.size()); + ASSERT_EQ(0u, trail.size()); + ASSERT_EQ("moi", args["abc"]); + } + + { + RestApiPath uri("/*"); + ASSERT_TRUE(uri.Match(args, trail, "/a/b/c")); + ASSERT_EQ(0u, args.size()); + ASSERT_EQ(3u, trail.size()); + ASSERT_EQ("a", trail[0]); + ASSERT_EQ("b", trail[1]); + ASSERT_EQ("c", trail[2]); + } +}
--- a/UnitTests/SQLite.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/UnitTests/SQLite.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -3,6 +3,7 @@ #include "../Core/Toolbox.h" #include "../Core/SQLite/Connection.h" #include "../Core/SQLite/Statement.h" +#include "../Core/SQLite/Transaction.h" #include <sqlite3.h> @@ -202,3 +203,36 @@ ASSERT_TRUE(func->deleted_.find(4300) != func->deleted_.end()); ASSERT_TRUE(func->deleted_.find(4301) != func->deleted_.end()); } + + +TEST(SQLite, EmptyTransactions) +{ + try + { + SQLite::Connection c; + c.OpenInMemory(); + + c.Execute("CREATE TABLE a(id INTEGER PRIMARY KEY);"); + c.Execute("INSERT INTO a VALUES(NULL)"); + + { + SQLite::Transaction t(c); + t.Begin(); + { + SQLite::Statement s(c, SQLITE_FROM_HERE, "SELECT * FROM a"); + s.Step(); + } + //t.Commit(); + } + + { + SQLite::Statement s(c, SQLITE_FROM_HERE, "SELECT * FROM a"); + s.Step(); + } + } + catch (OrthancException& e) + { + fprintf(stderr, "Exception: [%s]\n", e.What()); + throw e; + } +}
--- a/UnitTests/SQLiteChromium.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/UnitTests/SQLiteChromium.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,8 +7,9 @@ #include <sqlite3.h> + using namespace Orthanc; - +using namespace Orthanc::SQLite; /******************************************************************** @@ -37,13 +38,13 @@ db_.Close(); } - SQLite::Connection& db() + Connection& db() { return db_; } private: - SQLite::Connection db_; + Connection db_; }; @@ -67,17 +68,17 @@ db().ExecuteAndReturnErrorCode("CREATE TABLE TABLE")); ASSERT_EQ(SQLITE_ERROR, db().ExecuteAndReturnErrorCode( - "INSERT INTO foo(a, b) VALUES (1, 2, 3, 4)")); + "INSERT INTO foo(a, b) VALUES (1, 2, 3, 4)")); } TEST_F(SQLConnectionTest, CachedStatement) { - SQLite::StatementId id1("foo", 12); + StatementId id1("foo", 12); ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); ASSERT_TRUE(db().Execute("INSERT INTO foo(a, b) VALUES (12, 13)")); // Create a new cached statement. { - SQLite::Statement s(db(), id1, "SELECT a FROM foo"); + Statement s(db(), id1, "SELECT a FROM foo"); ASSERT_TRUE(s.Step()); EXPECT_EQ(12, s.ColumnInt(0)); } @@ -88,7 +89,7 @@ { // Get the same statement using different SQL. This should ignore our // SQL and use the cached one (so it will be valid). - SQLite::Statement s(db(), id1, "something invalid("); + Statement s(db(), id1, "something invalid("); ASSERT_TRUE(s.Step()); EXPECT_EQ(12, s.ColumnInt(0)); } @@ -132,7 +133,7 @@ EXPECT_LT(0, row); // It should be the primary key of the row we just inserted. - SQLite::Statement s(db(), "SELECT value FROM foo WHERE id=?"); + Statement s(db(), "SELECT value FROM foo WHERE id=?"); s.BindInt64(0, row); ASSERT_TRUE(s.Step()); EXPECT_EQ(12, s.ColumnInt(0)); @@ -155,65 +156,70 @@ ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/statement_unittest.cc ********************************************************************/ -class SQLStatementTest : public SQLConnectionTest +namespace Orthanc { -}; - + namespace SQLite + { + class SQLStatementTest : public SQLConnectionTest + { + }; -TEST_F(SQLStatementTest, Run) { - ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); - ASSERT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (3, 12)")); + TEST_F(SQLStatementTest, Run) { + ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); + ASSERT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (3, 12)")); - SQLite::Statement s(db(), "SELECT b FROM foo WHERE a=?"); - // Stepping it won't work since we haven't bound the value. - EXPECT_FALSE(s.Step()); + Statement s(db(), "SELECT b FROM foo WHERE a=?"); + // Stepping it won't work since we haven't bound the value. + EXPECT_FALSE(s.Step()); - // Run should fail since this produces output, and we should use Step(). This - // gets a bit wonky since sqlite says this is OK so succeeded is set. - s.Reset(true); - s.BindInt(0, 3); - EXPECT_FALSE(s.Run()); - EXPECT_EQ(SQLITE_ROW, db().GetErrorCode()); + // Run should fail since this produces output, and we should use Step(). This + // gets a bit wonky since sqlite says this is OK so succeeded is set. + s.Reset(true); + s.BindInt(0, 3); + EXPECT_FALSE(s.Run()); + EXPECT_EQ(SQLITE_ROW, db().GetErrorCode()); - // Resetting it should put it back to the previous state (not runnable). - s.Reset(true); + // Resetting it should put it back to the previous state (not runnable). + s.Reset(true); - // Binding and stepping should produce one row. - s.BindInt(0, 3); - EXPECT_TRUE(s.Step()); - EXPECT_EQ(12, s.ColumnInt(0)); - EXPECT_FALSE(s.Step()); -} + // Binding and stepping should produce one row. + s.BindInt(0, 3); + EXPECT_TRUE(s.Step()); + EXPECT_EQ(12, s.ColumnInt(0)); + EXPECT_FALSE(s.Step()); + } -TEST_F(SQLStatementTest, BasicErrorCallback) { - ASSERT_TRUE(db().Execute("CREATE TABLE foo (a INTEGER PRIMARY KEY, b)")); - // Insert in the foo table the primary key. It is an error to insert - // something other than an number. This error causes the error callback - // handler to be called with SQLITE_MISMATCH as error code. - SQLite::Statement s(db(), "INSERT INTO foo (a) VALUES (?)"); - s.BindCString(0, "bad bad"); - EXPECT_THROW(s.Run(), OrthancException); -} + TEST_F(SQLStatementTest, BasicErrorCallback) { + ASSERT_TRUE(db().Execute("CREATE TABLE foo (a INTEGER PRIMARY KEY, b)")); + // Insert in the foo table the primary key. It is an error to insert + // something other than an number. This error causes the error callback + // handler to be called with SQLITE_MISMATCH as error code. + Statement s(db(), "INSERT INTO foo (a) VALUES (?)"); + s.BindCString(0, "bad bad"); + EXPECT_THROW(s.Run(), OrthancException); + } -TEST_F(SQLStatementTest, Reset) { - ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); - ASSERT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (3, 12)")); - ASSERT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (4, 13)")); + TEST_F(SQLStatementTest, Reset) { + ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); + ASSERT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (3, 12)")); + ASSERT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (4, 13)")); - SQLite::Statement s(db(), "SELECT b FROM foo WHERE a = ? "); - s.BindInt(0, 3); - ASSERT_TRUE(s.Step()); - EXPECT_EQ(12, s.ColumnInt(0)); - ASSERT_FALSE(s.Step()); + Statement s(db(), "SELECT b FROM foo WHERE a = ? "); + s.BindInt(0, 3); + ASSERT_TRUE(s.Step()); + EXPECT_EQ(12, s.ColumnInt(0)); + ASSERT_FALSE(s.Step()); - s.Reset(false); - // Verify that we can get all rows again. - ASSERT_TRUE(s.Step()); - EXPECT_EQ(12, s.ColumnInt(0)); - EXPECT_FALSE(s.Step()); + s.Reset(false); + // Verify that we can get all rows again. + ASSERT_TRUE(s.Step()); + EXPECT_EQ(12, s.ColumnInt(0)); + EXPECT_FALSE(s.Step()); - s.Reset(true); - ASSERT_FALSE(s.Step()); + s.Reset(true); + ASSERT_FALSE(s.Step()); + } + } } @@ -221,7 +227,6 @@ - /******************************************************************** ** Tests from ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/transaction_unittest.cc @@ -239,7 +244,7 @@ // Returns the number of rows in table "foo". int CountFoo() { - SQLite::Statement count(db(), "SELECT count(*) FROM foo"); + Statement count(db(), "SELECT count(*) FROM foo"); count.Step(); return count.ColumnInt(0); } @@ -248,7 +253,7 @@ TEST_F(SQLTransactionTest, Commit) { { - SQLite::Transaction t(db()); + Transaction t(db()); EXPECT_FALSE(t.IsOpen()); t.Begin(); EXPECT_TRUE(t.IsOpen()); @@ -266,7 +271,7 @@ // Test some basic initialization, and that rollback runs when you exit the // scope. { - SQLite::Transaction t(db()); + Transaction t(db()); EXPECT_FALSE(t.IsOpen()); t.Begin(); EXPECT_TRUE(t.IsOpen()); @@ -278,7 +283,7 @@ EXPECT_EQ(0, CountFoo()); // Test explicit rollback. - SQLite::Transaction t2(db()); + Transaction t2(db()); EXPECT_FALSE(t2.IsOpen()); t2.Begin(); @@ -296,13 +301,13 @@ // Outermost transaction. { - SQLite::Transaction outer(db()); + Transaction outer(db()); outer.Begin(); EXPECT_EQ(1, db().GetTransactionNesting()); // The first inner one gets committed. { - SQLite::Transaction inner1(db()); + Transaction inner1(db()); inner1.Begin(); EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)")); EXPECT_EQ(2, db().GetTransactionNesting()); @@ -316,7 +321,7 @@ // The second inner one gets rolled back. { - SQLite::Transaction inner2(db()); + Transaction inner2(db()); inner2.Begin(); EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)")); EXPECT_EQ(2, db().GetTransactionNesting()); @@ -329,7 +334,7 @@ // back. EXPECT_EQ(1, db().GetTransactionNesting()); { - SQLite::Transaction inner3(db()); + Transaction inner3(db()); EXPECT_THROW(inner3.Begin(), OrthancException); EXPECT_EQ(1, db().GetTransactionNesting()); }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/UnitTests/ServerIndex.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -0,0 +1,398 @@ +#include "gtest/gtest.h" + +#include "../OrthancServer/DatabaseWrapper.h" +#include "../Core/Uuid.h" + +#include <ctype.h> +#include <glog/logging.h> + +using namespace Orthanc; + +namespace +{ + class ServerIndexListener : public IServerIndexListener + { + public: + std::vector<std::string> deletedFiles_; + std::string ancestorId_; + ResourceType ancestorType_; + + void Reset() + { + ancestorId_ = ""; + deletedFiles_.clear(); + } + + virtual void SignalRemainingAncestor(ResourceType type, + const std::string& publicId) + { + ancestorId_ = publicId; + ancestorType_ = type; + } + + virtual void SignalFileDeleted(const FileInfo& info) + { + const std::string fileUuid = info.GetUuid(); + deletedFiles_.push_back(fileUuid); + LOG(INFO) << "A file must be removed: " << fileUuid; + } + }; +} + + +TEST(DatabaseWrapper, Simple) +{ + ServerIndexListener listener; + DatabaseWrapper index(listener); + + int64_t a[] = { + index.CreateResource("a", ResourceType_Patient), // 0 + index.CreateResource("b", ResourceType_Study), // 1 + index.CreateResource("c", ResourceType_Series), // 2 + index.CreateResource("d", ResourceType_Instance), // 3 + index.CreateResource("e", ResourceType_Instance), // 4 + index.CreateResource("f", ResourceType_Instance), // 5 + index.CreateResource("g", ResourceType_Study) // 6 + }; + + ASSERT_EQ("a", index.GetPublicId(a[0])); + ASSERT_EQ("b", index.GetPublicId(a[1])); + ASSERT_EQ("c", index.GetPublicId(a[2])); + ASSERT_EQ("d", index.GetPublicId(a[3])); + ASSERT_EQ("e", index.GetPublicId(a[4])); + ASSERT_EQ("f", index.GetPublicId(a[5])); + ASSERT_EQ("g", index.GetPublicId(a[6])); + + { + Json::Value t; + index.GetAllPublicIds(t, ResourceType_Patient); + + ASSERT_EQ(1u, t.size()); + ASSERT_EQ("a", t[0u].asString()); + + index.GetAllPublicIds(t, ResourceType_Series); + ASSERT_EQ(1u, t.size()); + ASSERT_EQ("c", t[0u].asString()); + + index.GetAllPublicIds(t, ResourceType_Study); + ASSERT_EQ(2u, t.size()); + + index.GetAllPublicIds(t, ResourceType_Instance); + ASSERT_EQ(3u, t.size()); + } + + index.SetGlobalProperty(GlobalProperty_FlushSleep, "World"); + + index.AttachChild(a[0], a[1]); + index.AttachChild(a[1], a[2]); + index.AttachChild(a[2], a[3]); + index.AttachChild(a[2], a[4]); + index.AttachChild(a[6], a[5]); + + int64_t parent; + ASSERT_FALSE(index.LookupParent(parent, a[0])); + ASSERT_TRUE(index.LookupParent(parent, a[1])); ASSERT_EQ(a[0], parent); + ASSERT_TRUE(index.LookupParent(parent, a[2])); ASSERT_EQ(a[1], parent); + ASSERT_TRUE(index.LookupParent(parent, a[3])); ASSERT_EQ(a[2], parent); + ASSERT_TRUE(index.LookupParent(parent, a[4])); ASSERT_EQ(a[2], parent); + ASSERT_TRUE(index.LookupParent(parent, a[5])); ASSERT_EQ(a[6], parent); + ASSERT_FALSE(index.LookupParent(parent, a[6])); + + std::string s; + + ASSERT_FALSE(index.GetParentPublicId(s, a[0])); + ASSERT_FALSE(index.GetParentPublicId(s, a[6])); + ASSERT_TRUE(index.GetParentPublicId(s, a[1])); ASSERT_EQ("a", s); + ASSERT_TRUE(index.GetParentPublicId(s, a[2])); ASSERT_EQ("b", s); + ASSERT_TRUE(index.GetParentPublicId(s, a[3])); ASSERT_EQ("c", s); + ASSERT_TRUE(index.GetParentPublicId(s, a[4])); ASSERT_EQ("c", s); + ASSERT_TRUE(index.GetParentPublicId(s, a[5])); ASSERT_EQ("g", s); + + std::list<std::string> l; + index.GetChildrenPublicId(l, a[0]); ASSERT_EQ(1u, l.size()); ASSERT_EQ("b", l.front()); + index.GetChildrenPublicId(l, a[1]); ASSERT_EQ(1u, l.size()); ASSERT_EQ("c", l.front()); + index.GetChildrenPublicId(l, a[3]); ASSERT_EQ(0u, l.size()); + index.GetChildrenPublicId(l, a[4]); ASSERT_EQ(0u, l.size()); + index.GetChildrenPublicId(l, a[5]); ASSERT_EQ(0u, l.size()); + index.GetChildrenPublicId(l, a[6]); ASSERT_EQ(1u, l.size()); ASSERT_EQ("f", l.front()); + + index.GetChildrenPublicId(l, a[2]); ASSERT_EQ(2u, l.size()); + if (l.front() == "d") + { + ASSERT_EQ("e", l.back()); + } + else + { + ASSERT_EQ("d", l.back()); + ASSERT_EQ("e", l.front()); + } + + index.AddAttachment(a[4], FileInfo("my json file", FileContentType_Json, 42, CompressionType_Zlib, 21)); + index.AddAttachment(a[4], FileInfo("my dicom file", FileContentType_Dicom, 42)); + index.AddAttachment(a[6], FileInfo("world", FileContentType_Dicom, 44)); + index.SetMetadata(a[4], MetadataType_Instance_RemoteAet, "PINNACLE"); + + ASSERT_EQ(21u + 42u + 44u, index.GetTotalCompressedSize()); + ASSERT_EQ(42u + 42u + 44u, index.GetTotalUncompressedSize()); + + DicomMap m; + m.SetValue(0x0010, 0x0010, "PatientName"); + index.SetMainDicomTags(a[3], m); + + int64_t b; + ResourceType t; + ASSERT_TRUE(index.LookupResource("g", b, t)); + ASSERT_EQ(7, b); + ASSERT_EQ(ResourceType_Study, t); + + ASSERT_TRUE(index.LookupMetadata(s, a[4], MetadataType_Instance_RemoteAet)); + ASSERT_FALSE(index.LookupMetadata(s, a[4], MetadataType_Instance_IndexInSeries)); + ASSERT_EQ("PINNACLE", s); + ASSERT_EQ("PINNACLE", index.GetMetadata(a[4], MetadataType_Instance_RemoteAet)); + ASSERT_EQ("None", index.GetMetadata(a[4], MetadataType_Instance_IndexInSeries, "None")); + + ASSERT_TRUE(index.LookupGlobalProperty(s, GlobalProperty_FlushSleep)); + ASSERT_FALSE(index.LookupGlobalProperty(s, static_cast<GlobalProperty>(42))); + ASSERT_EQ("World", s); + ASSERT_EQ("World", index.GetGlobalProperty(GlobalProperty_FlushSleep)); + ASSERT_EQ("None", index.GetGlobalProperty(static_cast<GlobalProperty>(42), "None")); + + FileInfo att; + ASSERT_TRUE(index.LookupAttachment(att, a[4], FileContentType_Json)); + ASSERT_EQ("my json file", att.GetUuid()); + ASSERT_EQ(21u, att.GetCompressedSize()); + ASSERT_EQ(42u, att.GetUncompressedSize()); + ASSERT_EQ(CompressionType_Zlib, att.GetCompressionType()); + + ASSERT_EQ(0u, listener.deletedFiles_.size()); + ASSERT_EQ(7u, index.GetTableRecordCount("Resources")); + ASSERT_EQ(3u, index.GetTableRecordCount("AttachedFiles")); + ASSERT_EQ(1u, index.GetTableRecordCount("Metadata")); + ASSERT_EQ(1u, index.GetTableRecordCount("MainDicomTags")); + index.DeleteResource(a[0]); + + ASSERT_EQ(2u, listener.deletedFiles_.size()); + 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")); + ASSERT_EQ(1u, index.GetTableRecordCount("AttachedFiles")); + ASSERT_EQ(0u, index.GetTableRecordCount("MainDicomTags")); + index.DeleteResource(a[5]); + ASSERT_EQ(0u, index.GetTableRecordCount("Resources")); + ASSERT_EQ(0u, index.GetTableRecordCount("AttachedFiles")); + ASSERT_EQ(2u, index.GetTableRecordCount("GlobalProperties")); + + ASSERT_EQ(3u, listener.deletedFiles_.size()); + ASSERT_FALSE(std::find(listener.deletedFiles_.begin(), + listener.deletedFiles_.end(), + "world") == listener.deletedFiles_.end()); +} + + + + +TEST(DatabaseWrapper, Upward) +{ + ServerIndexListener listener; + DatabaseWrapper index(listener); + + int64_t a[] = { + index.CreateResource("a", ResourceType_Patient), // 0 + index.CreateResource("b", ResourceType_Study), // 1 + index.CreateResource("c", ResourceType_Series), // 2 + index.CreateResource("d", ResourceType_Instance), // 3 + index.CreateResource("e", ResourceType_Instance), // 4 + index.CreateResource("f", ResourceType_Study), // 5 + index.CreateResource("g", ResourceType_Series), // 6 + index.CreateResource("h", ResourceType_Series) // 7 + }; + + index.AttachChild(a[0], a[1]); + index.AttachChild(a[1], a[2]); + index.AttachChild(a[2], a[3]); + index.AttachChild(a[2], a[4]); + index.AttachChild(a[1], a[6]); + index.AttachChild(a[0], a[5]); + index.AttachChild(a[5], a[7]); + + { + Json::Value j; + index.GetChildren(j, a[0]); + ASSERT_EQ(2u, j.size()); + ASSERT_TRUE((j[0u] == "b" && j[1u] == "f") || + (j[1u] == "b" && j[0u] == "f")); + + index.GetChildren(j, a[1]); + ASSERT_EQ(2u, j.size()); + ASSERT_TRUE((j[0u] == "c" && j[1u] == "g") || + (j[1u] == "c" && j[0u] == "g")); + + index.GetChildren(j, a[2]); + ASSERT_EQ(2u, j.size()); + ASSERT_TRUE((j[0u] == "d" && j[1u] == "e") || + (j[1u] == "d" && j[0u] == "e")); + + index.GetChildren(j, a[3]); ASSERT_EQ(0u, j.size()); + index.GetChildren(j, a[4]); ASSERT_EQ(0u, j.size()); + index.GetChildren(j, a[5]); ASSERT_EQ(1u, j.size()); ASSERT_EQ("h", j[0u].asString()); + index.GetChildren(j, a[6]); ASSERT_EQ(0u, j.size()); + index.GetChildren(j, a[7]); ASSERT_EQ(0u, j.size()); + } + + listener.Reset(); + index.DeleteResource(a[3]); + ASSERT_EQ("c", listener.ancestorId_); + ASSERT_EQ(ResourceType_Series, listener.ancestorType_); + + listener.Reset(); + index.DeleteResource(a[4]); + ASSERT_EQ("b", listener.ancestorId_); + ASSERT_EQ(ResourceType_Study, listener.ancestorType_); + + listener.Reset(); + index.DeleteResource(a[7]); + ASSERT_EQ("a", listener.ancestorId_); + ASSERT_EQ(ResourceType_Patient, listener.ancestorType_); + + listener.Reset(); + 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")); +}
--- a/UnitTests/Versions.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/UnitTests/Versions.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -7,6 +7,7 @@ #include <zlib.h> #include <curl/curl.h> #include <boost/version.hpp> +#include <sqlite3.h> TEST(Versions, Zlib) @@ -26,6 +27,18 @@ png_access_version_number()); } +TEST(Versions, SQLite) +{ + // http://www.sqlite.org/capi3ref.html#sqlite3_libversion + assert(sqlite3_libversion_number() == SQLITE_VERSION_NUMBER ); + assert(strcmp(sqlite3_sourceid(), SQLITE_SOURCE_ID) == 0); + assert(strcmp(sqlite3_libversion(), SQLITE_VERSION) == 0); + + // Ensure that the SQLite version is above 3.7.0. + // "sqlite3_create_function_v2" is not defined in previous versions. + ASSERT_GE(SQLITE_VERSION_NUMBER, 3007000); +} + #if ORTHANC_STATIC == 1 TEST(Versions, ZlibStatic)
--- a/UnitTests/Zip.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/UnitTests/Zip.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -2,14 +2,18 @@ #include "../Core/OrthancException.h" #include "../Core/Compression/ZipWriter.h" +#include "../Core/Compression/HierarchicalZipWriter.h" +#include "../Core/Toolbox.h" +using namespace Orthanc; + TEST(ZipWriter, Basic) { Orthanc::ZipWriter w; w.SetOutputPath("hello.zip"); w.Open(); - w.CreateFileInZip("world/hello"); + w.OpenFile("world/hello"); w.Write("Hello world"); } @@ -22,3 +26,97 @@ w.Open(); ASSERT_THROW(w.Write("hello world"), Orthanc::OrthancException); } + + + + + +namespace Orthanc +{ + // The namespace is necessary + // http://code.google.com/p/googletest/wiki/AdvancedGuide#Private_Class_Members + + TEST(HierarchicalZipWriter, Index) + { + HierarchicalZipWriter::Index i; + ASSERT_EQ("hello", i.OpenFile("hello")); + ASSERT_EQ("hello-2", i.OpenFile("hello")); + ASSERT_EQ("coucou", i.OpenFile("coucou")); + ASSERT_EQ("hello-3", i.OpenFile("hello")); + + i.OpenDirectory("coucou"); + + ASSERT_EQ("coucou-2/world", i.OpenFile("world")); + ASSERT_EQ("coucou-2/world-2", i.OpenFile("world")); + + i.OpenDirectory("world"); + + ASSERT_EQ("coucou-2/world-3/hello", i.OpenFile("hello")); + ASSERT_EQ("coucou-2/world-3/hello-2", i.OpenFile("hello")); + + i.CloseDirectory(); + + ASSERT_EQ("coucou-2/world-4", i.OpenFile("world")); + + i.CloseDirectory(); + + ASSERT_EQ("coucou-3", i.OpenFile("coucou")); + + ASSERT_THROW(i.CloseDirectory(), OrthancException); + } + + + TEST(HierarchicalZipWriter, Filenames) + { + ASSERT_EQ("trE hell", HierarchicalZipWriter::Index::KeepAlphanumeric(" ÊtrE hellô ")); + + // The "^" character is considered as a space in DICOM + ASSERT_EQ("Hel lo world", HierarchicalZipWriter::Index::KeepAlphanumeric(" Hel^^ ^\r\n\t^^lo \t <world> ")); + } +} + + +TEST(HierarchicalZipWriter, Basic) +{ + static const std::string SPACES = " "; + + HierarchicalZipWriter w("hello2.zip"); + + w.SetCompressionLevel(0); + + // Inside "/" + w.OpenFile("hello"); + w.Write(SPACES + "hello\n"); + w.OpenFile("hello"); + w.Write(SPACES + "hello-2\n"); + w.OpenDirectory("hello"); + + // Inside "/hello-3" + w.OpenFile("hello"); + w.Write(SPACES + "hello\n"); + w.OpenDirectory("hello"); + + w.SetCompressionLevel(9); + + // Inside "/hello-3/hello-2" + w.OpenFile("hello"); + w.Write(SPACES + "hello\n"); + w.OpenFile("hello"); + w.Write(SPACES + "hello-2\n"); + w.CloseDirectory(); + + // Inside "/hello-3" + w.OpenFile("hello"); + w.Write(SPACES + "hello-3\n"); + + /** + + TO CHECK THE CONTENT OF THE "hello2.zip" FILE: + + # unzip -v hello2.zip + + => There must be 6 files. The first 3 files must have a negative + compression ratio. + + **/ +}
--- a/UnitTests/main.cpp Thu Oct 04 15:09:56 2012 +0200 +++ b/UnitTests/main.cpp Fri Dec 14 11:24:24 2012 +0100 @@ -4,7 +4,6 @@ #include "../Core/Compression/ZlibCompressor.h" #include "../Core/DicomFormat/DicomTag.h" -#include "../Core/FileStorage.h" #include "../OrthancCppClient/HttpClient.h" #include "../Core/HttpServer/HttpHandler.h" #include "../Core/OrthancException.h" @@ -98,51 +97,6 @@ ASSERT_EQ(a["aaa"], ""); } -TEST(FileStorage, Basic) -{ - FileStorage s("FileStorageUnitTests"); - - std::string data = Toolbox::GenerateUuid(); - std::string uid = s.Create(data); - std::string d; - s.ReadFile(d, uid); - ASSERT_EQ(d.size(), data.size()); - ASSERT_FALSE(memcmp(&d[0], &data[0], data.size())); -} - -TEST(FileStorage, EndToEnd) -{ - FileStorage s("FileStorageUnitTests"); - s.Clear(); - - std::list<std::string> u; - for (unsigned int i = 0; i < 10; i++) - { - u.push_back(s.Create(Toolbox::GenerateUuid())); - } - - std::set<std::string> ss; - s.ListAllFiles(ss); - ASSERT_EQ(10u, ss.size()); - - unsigned int c = 0; - for (std::list<std::string>::iterator - i = u.begin(); i != u.end(); i++, c++) - { - ASSERT_TRUE(ss.find(*i) != ss.end()); - if (c < 5) - s.Remove(*i); - } - - s.ListAllFiles(ss); - ASSERT_EQ(5u, ss.size()); - - s.Clear(); - s.ListAllFiles(ss); - ASSERT_EQ(0u, ss.size()); -} - - TEST(DicomFormat, Tag) { ASSERT_EQ("PatientName", FromDcmtkBridge::GetName(DicomTag(0x0010, 0x0010))); @@ -188,6 +142,7 @@ ASSERT_THROW(Toolbox::SplitUriComponents(c, ""), OrthancException); ASSERT_THROW(Toolbox::SplitUriComponents(c, "a"), OrthancException); + ASSERT_THROW(Toolbox::SplitUriComponents(c, "/coucou//coucou"), OrthancException); } @@ -266,6 +221,17 @@ ASSERT_EQ("d41d8cd98f00b204e9800998ecf8427e", s); } +TEST(Toolbox, ComputeSHA1) +{ + std::string s; + + Toolbox::ComputeSHA1(s, "The quick brown fox jumps over the lazy dog"); + ASSERT_EQ("2fd4e1c6-7a2d28fc-ed849ee1-bb76e739-1b93eb12", s); + Toolbox::ComputeSHA1(s, ""); + ASSERT_EQ("da39a3ee-5e6b4b0d-3255bfef-95601890-afd80709", s); +} + + TEST(Toolbox, Base64) { ASSERT_EQ("", Toolbox::EncodeBase64("")); @@ -279,6 +245,13 @@ printf("[%s]\n", Toolbox::GetDirectoryOfExecutable().c_str()); } +TEST(Toolbox, StripSpaces) +{ + ASSERT_EQ("", Toolbox::StripSpaces(" \t \r \n ")); + ASSERT_EQ("coucou", Toolbox::StripSpaces(" coucou \t \r \n ")); + ASSERT_EQ("cou cou", Toolbox::StripSpaces(" cou cou \n ")); + ASSERT_EQ("c", Toolbox::StripSpaces(" \n\t c\r \n ")); +} #include <glog/logging.h> @@ -302,7 +275,7 @@ // Open in Emacs, then save with UTF-8 encoding, then "hexdump -C" std::string utf8 = Toolbox::ConvertToUtf8(s, "ISO-8859-1"); - ASSERT_EQ(15, utf8.size()); + ASSERT_EQ(15u, utf8.size()); ASSERT_EQ(0xc3, static_cast<unsigned char>(utf8[0])); ASSERT_EQ(0xa0, static_cast<unsigned char>(utf8[1])); ASSERT_EQ(0xc3, static_cast<unsigned char>(utf8[2])); @@ -325,6 +298,11 @@ { // Initialize Google's logging library. FLAGS_logtostderr = true; + FLAGS_minloglevel = 0; + + // Go to trace-level verbosity + //FLAGS_v = 1; + google::InitGoogleLogging("Orthanc"); OrthancInitialize();