Mercurial > hg > orthanc
changeset 1364:111e23bb4904 query-retrieve
integration mainline->query-retrieve
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Thu, 21 May 2015 16:58:30 +0200 |
parents | f894be6e7cc1 (current diff) feaf2840917c (diff) |
children | 38ce915cb455 |
files | Core/DicomFormat/DicomMap.cpp Core/DicomFormat/DicomMap.h Core/DicomFormat/DicomTag.h Core/FileStorage/FileStorage.cpp Core/FileStorage/FileStorage.h OrthancServer/DicomProtocol/DicomUserConnection.cpp OrthancServer/OrthancFindRequestHandler.cpp OrthancServer/ServerIndex.cpp OrthancServer/ServerIndex.h OrthancServer/main.cpp Resources/Patches/mongoose-patch.diff UnitTestsSources/DicomMap.cpp UnitTestsSources/FileStorage.cpp UnitTestsSources/FromDcmtk.cpp UnitTestsSources/JpegLossless.cpp UnitTestsSources/Lua.cpp UnitTestsSources/MemoryCache.cpp UnitTestsSources/MultiThreading.cpp UnitTestsSources/Png.cpp UnitTestsSources/RestApi.cpp UnitTestsSources/SQLite.cpp UnitTestsSources/SQLiteChromium.cpp UnitTestsSources/Versions.cpp UnitTestsSources/Zip.cpp |
diffstat | 383 files changed, 32178 insertions(+), 7198 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.travis.yml Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,68 @@ +language: cpp + +env: + - TRAVIS_MINGW=OFF + - TRAVIS_MINGW=ON + +compiler: + - gcc + - clang + +os: + - osx + - linux + +osx_image: xcode61 + +matrix: + exclude: + # This excludes OSX builds from the build matrix for gcc + - os: osx + compiler: gcc + + # Do not compile for OS X or clang when MinGW is enabled + - os: osx + env: TRAVIS_MINGW=ON + - compiler: clang + env: TRAVIS_MINGW=ON + +before_install: + - if [ $TRAVIS_OS_NAME == linux ]; then sudo apt-get update -qq && sudo apt-get install + -qq build-essential unzip cmake mercurial uuid-dev libcurl4-openssl-dev liblua5.1-0-dev + libgtest-dev libpng-dev libsqlite3-dev libssl-dev zlib1g-dev libdcmtk2-dev libwrap0-dev + libcharls-dev; fi + - if [ $TRAVIS_OS_NAME == linux -a $TRAVIS_MINGW == ON ]; then sudo apt-get install mingw32; fi + + +before_script: + - mkdir Build + - cd Build + - if [ $TRAVIS_OS_NAME == linux -a $TRAVIS_MINGW == OFF ]; then cmake + -DCMAKE_BUILD_TYPE=Debug "-DDCMTK_LIBRARIES=CharLS;dcmjpls;wrap;oflog" + -DALLOW_DOWNLOADS=ON -DUSE_SYSTEM_BOOST=OFF -DUSE_SYSTEM_MONGOOSE=OFF -DUSE_SYSTEM_JSONCPP=OFF + -DUSE_SYSTEM_GOOGLE_LOG=OFF -DUSE_SYSTEM_PUGIXML=OFF -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON + ..; fi + - if [ $TRAVIS_OS_NAME == linux -a $TRAVIS_MINGW == ON ]; then cmake + -DCMAKE_BUILD_TYPE=Debug -DSTATIC_BUILD=ON -DSTANDALONE_BUILD=ON -DALLOW_DOWNLOADS=ON + -DCMAKE_TOOLCHAIN_FILE=Resources/MinGWToolchain.cmake + ..; fi + - if [ $TRAVIS_OS_NAME == osx ]; then cmake + -DCMAKE_BUILD_TYPE=Debug -DSTATIC_BUILD=ON -DSTANDALONE_BUILD=ON -DALLOW_DOWNLOADS=ON + ..; fi + +script: make && if [ $TRAVIS_MINGW == OFF ]; then ./UnitTests; fi + +#script: cp ../README Orthanc +#deploy: +# provider: releases +# api_key: +# secure: WU+niKLAKMoJHST5EK23BayK4qXSrXELKlJYc8wRjMO4ay1KSgvzlY2UGKeW1EPClBfZZ0Uh5VKF8l34exsfirFuwCX2qceozduZproUszZ4Z88X8wt8Ctu8tBuuKLZYFc9iNH4zw+QZyRuPyXK9iWpS0L9O20pqy5upTsagM3o= +# file_glob: true +# file: +# - 'Build/Orthanc' +# - 'Build/UnitTests' +# - 'BuildMinGW32/Orthanc.exe' +# - 'BuildMinGW32/UnitTests.exe' +# skip_cleanup: true +# on: +# all_branches: true
--- a/AUTHORS Wed Jun 25 15:34:40 2014 +0200 +++ b/AUTHORS Thu May 21 16:58:30 2015 +0200 @@ -6,9 +6,11 @@ ------------------ * Sebastien Jodogne <s.jodogne@gmail.com> - Department of Medical Physics, CHU of Liege, Belgium + Department of Medical Physics + University Hospital of Liege + Belgium - Overall design and main developper. + Overall design and lead developer. Client library
--- a/CMakeLists.txt Wed Jun 25 15:34:40 2014 +0200 +++ b/CMakeLists.txt Thu May 21 16:58:30 2015 +0200 @@ -34,22 +34,33 @@ SET(USE_SYSTEM_CURL ON CACHE BOOL "Use the system version of LibCurl") SET(USE_SYSTEM_OPENSSL ON CACHE BOOL "Use the system version of OpenSSL") SET(USE_SYSTEM_ZLIB ON CACHE BOOL "Use the system version of ZLib") +SET(USE_SYSTEM_PUGIXML ON CACHE BOOL "Use the system version of Pugixml)") + +# Experimental options +SET(USE_PUGIXML ON CACHE BOOL "Use the Pugixml parser (turn off only for debug)") # Distribution-specific settings SET(USE_GTEST_DEBIAN_SOURCE_PACKAGE OFF CACHE BOOL "Use the sources of Google Test shipped with libgtest-dev (Debian only)") +SET(SYSTEM_MONGOOSE_USE_CALLBACKS ON CACHE BOOL "The system version of Mongoose uses callbacks (version >= 3.7)") +SET(USE_BOOST_ICONV ON CACHE BOOL "Use iconv instead of wconv (Windows only)") + mark_as_advanced(USE_GTEST_DEBIAN_SOURCE_PACKAGE) +mark_as_advanced(SYSTEM_MONGOOSE_USE_CALLBACKS) +mark_as_advanced(USE_BOOST_ICONV) + +# Path to the root folder of the Orthanc distribution +set(ORTHANC_ROOT ${CMAKE_SOURCE_DIR}) # Some basic inclusions include(CheckIncludeFiles) include(CheckIncludeFileCXX) include(CheckLibraryExists) +include(FindPythonInterp) include(${CMAKE_SOURCE_DIR}/Resources/CMake/AutoGeneratedCode.cmake) include(${CMAKE_SOURCE_DIR}/Resources/CMake/DownloadPackage.cmake) include(${CMAKE_SOURCE_DIR}/Resources/CMake/Compiler.cmake) include(${CMAKE_SOURCE_DIR}/Resources/CMake/VisualStudioPrecompiledHeaders.cmake) -set(ORTHANC_ROOT ${CMAKE_SOURCE_DIR}) - @@ -72,7 +83,7 @@ Core/DicomFormat/DicomIntegerPixelAccessor.cpp Core/DicomFormat/DicomInstanceHasher.cpp Core/Enumerations.cpp - Core/FileStorage/FileStorage.cpp + Core/FileStorage/FilesystemStorage.cpp Core/FileStorage/StorageAccessor.cpp Core/FileStorage/CompressedFileStorageAccessor.cpp Core/FileStorage/FileStorageAccessor.cpp @@ -84,6 +95,9 @@ Core/HttpServer/MongooseServer.cpp Core/HttpServer/HttpFileSender.cpp Core/HttpServer/FilesystemHttpSender.cpp + Core/RestApi/RestApiCall.cpp + Core/RestApi/RestApiGetCall.cpp + Core/RestApi/RestApiHierarchy.cpp Core/RestApi/RestApiPath.cpp Core/RestApi/RestApiOutput.cpp Core/RestApi/RestApi.cpp @@ -91,6 +105,7 @@ Core/MultiThreading/BagOfRunnablesBySteps.cpp Core/MultiThreading/Mutex.cpp Core/MultiThreading/ReaderWriterLock.cpp + Core/MultiThreading/Semaphore.cpp Core/MultiThreading/SharedMessageQueue.cpp Core/MultiThreading/ThreadedCommandProcessor.cpp Core/ImageFormats/ImageAccessor.cpp @@ -114,6 +129,11 @@ OrthancCppClient/Series.cpp OrthancCppClient/Instance.cpp OrthancCppClient/Patient.cpp + + Plugins/Engine/SharedLibrary.cpp + Plugins/Engine/PluginsManager.cpp + Plugins/Engine/OrthancPlugins.cpp + Plugins/Engine/OrthancPluginDatabase.cpp ) @@ -126,6 +146,7 @@ OrthancServer/DicomModification.cpp OrthancServer/FromDcmtkBridge.cpp OrthancServer/ParsedDicomFile.cpp + OrthancServer/DicomDirWriter.cpp OrthancServer/Internals/CommandDispatcher.cpp OrthancServer/Internals/FindScp.cpp OrthancServer/Internals/MoveScp.cpp @@ -148,26 +169,51 @@ OrthancServer/ServerToolbox.cpp OrthancServer/OrthancFindRequestHandler.cpp OrthancServer/OrthancMoveRequestHandler.cpp + OrthancServer/ExportedResource.cpp + OrthancServer/ResourceFinder.cpp + OrthancServer/DicomFindQuery.cpp + + # From "lua-scripting" branch + OrthancServer/DicomInstanceToStore.cpp + OrthancServer/Scheduler/DeleteInstanceCommand.cpp + OrthancServer/Scheduler/ModifyInstanceCommand.cpp + OrthancServer/Scheduler/ServerCommandInstance.cpp + OrthancServer/Scheduler/ServerJob.cpp + OrthancServer/Scheduler/ServerScheduler.cpp + OrthancServer/Scheduler/StorePeerCommand.cpp + OrthancServer/Scheduler/StoreScuCommand.cpp + OrthancServer/Scheduler/CallSystemCommand.cpp ) set(ORTHANC_UNIT_TESTS_SOURCES - UnitTestsSources/DicomMap.cpp - UnitTestsSources/FileStorage.cpp - UnitTestsSources/FromDcmtk.cpp - UnitTestsSources/MemoryCache.cpp - UnitTestsSources/Png.cpp - UnitTestsSources/RestApi.cpp - UnitTestsSources/SQLite.cpp - UnitTestsSources/SQLiteChromium.cpp + UnitTestsSources/DicomMapTests.cpp + UnitTestsSources/FileStorageTests.cpp + UnitTestsSources/FromDcmtkTests.cpp + UnitTestsSources/MemoryCacheTests.cpp + UnitTestsSources/PngTests.cpp + UnitTestsSources/RestApiTests.cpp + UnitTestsSources/SQLiteTests.cpp + UnitTestsSources/SQLiteChromiumTests.cpp UnitTestsSources/ServerIndexTests.cpp - UnitTestsSources/Versions.cpp - UnitTestsSources/Zip.cpp - UnitTestsSources/Lua.cpp - UnitTestsSources/MultiThreading.cpp + UnitTestsSources/VersionsTests.cpp + UnitTestsSources/ZipTests.cpp + UnitTestsSources/LuaTests.cpp + UnitTestsSources/MultiThreadingTests.cpp UnitTestsSources/UnitTestsMain.cpp UnitTestsSources/ImageProcessingTests.cpp - UnitTestsSources/JpegLossless.cpp + UnitTestsSources/JpegLosslessTests.cpp + UnitTestsSources/PluginsTests.cpp + ) + + +set(ORTHANC_EMBEDDED_FILES + PREPARE_DATABASE ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/PrepareDatabase.sql + UPGRADE_DATABASE_3_TO_4 ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Upgrade3To4.sql + UPGRADE_DATABASE_4_TO_5 ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Upgrade4To5.sql + CONFIGURATION_SAMPLE ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Configuration.json + DICOM_CONFORMANCE_STATEMENT ${CMAKE_CURRENT_SOURCE_DIR}/Resources/DicomConformanceStatement.txt + LUA_TOOLBOX ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Toolbox.lua ) @@ -198,6 +244,7 @@ include(${CMAKE_SOURCE_DIR}/Resources/CMake/LibPngConfiguration.cmake) include(${CMAKE_SOURCE_DIR}/Resources/CMake/LuaConfiguration.cmake) include(${CMAKE_SOURCE_DIR}/Resources/CMake/LibCurlConfiguration.cmake) +include(${CMAKE_SOURCE_DIR}/Resources/CMake/PugixmlConfiguration.cmake) if (${ENABLE_SSL}) @@ -227,19 +274,11 @@ ## Autogeneration of files ##################################################################### -# Prepare the embedded files -set(EMBEDDED_FILES - PREPARE_DATABASE ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/PrepareDatabase.sql - UPGRADE_DATABASE_3_TO_4 ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Upgrade3To4.sql - CONFIGURATION_SAMPLE ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Configuration.json - LUA_TOOLBOX ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Toolbox.lua - ) - if (${STANDALONE_BUILD}) # We embed all the resources in the binaries for standalone builds add_definitions(-DORTHANC_STANDALONE=1) EmbedResources( - ${EMBEDDED_FILES} + ${ORTHANC_EMBEDDED_FILES} ORTHANC_EXPLORER ${CMAKE_CURRENT_SOURCE_DIR}/OrthancExplorer ${DCMTK_DICTIONARIES} ) @@ -249,7 +288,7 @@ -DORTHANC_PATH=\"${CMAKE_SOURCE_DIR}\" ) EmbedResources( - ${EMBEDDED_FILES} + ${ORTHANC_EMBEDDED_FILES} ) endif() @@ -309,7 +348,7 @@ OrthancServer/main.cpp ) -target_link_libraries(Orthanc ServerLibrary CoreLibrary ${STATIC_LUA} ${STATIC_GOOGLE_LOG}) +target_link_libraries(Orthanc ServerLibrary CoreLibrary ${STATIC_LUA} ${STATIC_GOOGLE_LOG} ${DCMTK_LIBRARIES}) if (${OPENSSL_SOURCES_LENGTH} GREATER 0) target_link_libraries(Orthanc OpenSSL) @@ -338,7 +377,7 @@ ${GTEST_SOURCES} ${ORTHANC_UNIT_TESTS_SOURCES} ) -target_link_libraries(UnitTests ServerLibrary CoreLibrary ${STATIC_LUA} ${STATIC_GOOGLE_LOG}) +target_link_libraries(UnitTests ServerLibrary CoreLibrary ${STATIC_LUA} ${STATIC_GOOGLE_LOG} ${DCMTK_LIBRARIES}) if (${OPENSSL_SOURCES_LENGTH} GREATER 0) target_link_libraries(UnitTests OpenSSL) @@ -395,7 +434,8 @@ ) if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR - ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD") + ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") set_target_properties(OrthancClient PROPERTIES LINK_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined -Wl,--as-needed -Wl,--version-script=${ORTHANC_ROOT}/OrthancCppClient/SharedLibrary/Laaw/VersionScript.map" ) @@ -442,7 +482,11 @@ ) install( - FILES ${ORTHANC_ROOT}/OrthancCppClient/SharedLibrary/AUTOGENERATED/OrthancCppClient.h + FILES + ${ORTHANC_ROOT}/OrthancCppClient/SharedLibrary/AUTOGENERATED/OrthancCppClient.h + ${ORTHANC_ROOT}/Plugins/Include/OrthancCPlugin.h + ${ORTHANC_ROOT}/Plugins/Include/OrthancCDatabasePlugin.h + ${ORTHANC_ROOT}/Plugins/Include/OrthancCppDatabasePlugin.h DESTINATION include/orthanc ) endif() @@ -461,10 +505,26 @@ ${CMAKE_CURRENT_BINARY_DIR}/Orthanc.doxygen @ONLY) + configure_file( + ${CMAKE_SOURCE_DIR}/Resources/OrthancPlugin.doxygen + ${CMAKE_CURRENT_BINARY_DIR}/OrthancPlugin.doxygen + @ONLY) + add_custom_target(doc ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Orthanc.doxygen + COMMENT "Generating internal documentation with Doxygen" VERBATIM + ) + + add_custom_command(TARGET Orthanc + POST_BUILD + COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/OrthancPlugin.doxygen WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - COMMENT "Generating internal documentation with Doxygen" VERBATIM + COMMENT "Generating plugin documentation with Doxygen" VERBATIM + ) + + install( + DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/OrthancPluginDocumentation/doc/ + DESTINATION share/doc/orthanc/OrthancPlugin ) if (BUILD_CLIENT_LIBRARY)
--- a/Core/Cache/ICachePageProvider.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/Cache/ICachePageProvider.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/Cache/LeastRecentlyUsedIndex.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/Cache/LeastRecentlyUsedIndex.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/Cache/MemoryCache.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/Cache/MemoryCache.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/Cache/MemoryCache.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/Cache/MemoryCache.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/ChunkedBuffer.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/ChunkedBuffer.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/ChunkedBuffer.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/ChunkedBuffer.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/Compression/BufferCompressor.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/Compression/BufferCompressor.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/Compression/BufferCompressor.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/Compression/BufferCompressor.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/Compression/HierarchicalZipWriter.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/Compression/HierarchicalZipWriter.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -53,7 +53,7 @@ if (c == '^') c = ' '; - if (c < 128 && + if (c <= 127 && c >= 0) { if (isspace(c))
--- a/Core/Compression/HierarchicalZipWriter.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/Compression/HierarchicalZipWriter.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -63,8 +63,6 @@ Stack stack_; - std::string GetCurrentDirectoryPath() const; - std::string EnsureUniqueFilename(const char* filename); public: @@ -83,6 +81,8 @@ void CloseDirectory(); + std::string GetCurrentDirectoryPath() const; + static std::string KeepAlphanumeric(const std::string& source); }; @@ -114,12 +114,27 @@ return writer_.GetCompressionLevel(); } + void SetAppendToExisting(bool append) + { + writer_.SetAppendToExisting(append); + } + + bool IsAppendToExisting() const + { + return writer_.IsAppendToExisting(); + } + void OpenFile(const char* name); void OpenDirectory(const char* name); void CloseDirectory(); + std::string GetCurrentDirectoryPath() const + { + return indexer_.GetCurrentDirectoryPath(); + } + void Write(const char* data, size_t length) { writer_.Write(data, length);
--- a/Core/Compression/ZipWriter.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/Compression/ZipWriter.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -38,10 +38,11 @@ #include "ZipWriter.h" -#include "../../Resources/ThirdParty/minizip/zip.h" +#include <limits> +#include <boost/filesystem.hpp> #include <boost/date_time/posix_time/posix_time.hpp> -#include <limits> +#include "../../Resources/ThirdParty/minizip/zip.h" #include "../OrthancException.h" @@ -77,15 +78,19 @@ struct ZipWriter::PImpl { zipFile file_; + + PImpl() : file_(NULL) + { + } }; - ZipWriter::ZipWriter() : pimpl_(new PImpl) + ZipWriter::ZipWriter() : + pimpl_(new PImpl), + isZip64_(false), + hasFileInZip_(false), + append_(false), + compressionLevel_(6) { - compressionLevel_ = 6; - hasFileInZip_ = false; - isZip64_ = false; - - pimpl_->file_ = NULL; } ZipWriter::~ZipWriter() @@ -122,13 +127,20 @@ hasFileInZip_ = false; + int mode = APPEND_STATUS_CREATE; + if (append_ && + boost::filesystem::exists(path_)) + { + mode = APPEND_STATUS_ADDINZIP; + } + if (isZip64_) { - pimpl_->file_ = zipOpen64(path_.c_str(), APPEND_STATUS_CREATE); + pimpl_->file_ = zipOpen64(path_.c_str(), mode); } else { - pimpl_->file_ = zipOpen(path_.c_str(), APPEND_STATUS_CREATE); + pimpl_->file_ = zipOpen(path_.c_str(), mode); } if (!pimpl_->file_) @@ -230,4 +242,13 @@ length -= bytes; } } + + + void ZipWriter::SetAppendToExisting(bool append) + { + Close(); + append_ = append; + } + + }
--- a/Core/Compression/ZipWriter.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/Compression/ZipWriter.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -50,6 +50,7 @@ bool isZip64_; bool hasFileInZip_; + bool append_; uint8_t compressionLevel_; std::string path_; @@ -71,6 +72,13 @@ { return compressionLevel_; } + + void SetAppendToExisting(bool append); + + bool IsAppendToExisting() const + { + return append_; + } void Open();
--- a/Core/Compression/ZlibCompressor.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/Compression/ZlibCompressor.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/Compression/ZlibCompressor.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/Compression/ZlibCompressor.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/DicomFormat/DicomArray.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/DicomFormat/DicomArray.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/DicomFormat/DicomArray.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/DicomFormat/DicomArray.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/DicomFormat/DicomElement.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/DicomFormat/DicomElement.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/DicomFormat/DicomImageInformation.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/DicomFormat/DicomImageInformation.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -39,6 +39,7 @@ #include "DicomImageInformation.h" #include "../OrthancException.h" +#include "../Toolbox.h" #include <boost/lexical_cast.hpp> #include <limits> #include <cassert> @@ -53,6 +54,66 @@ try { + std::string p = values.GetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION).AsString(); + Toolbox::ToUpperCase(p); + + if (p == "RGB") + { + photometric_ = PhotometricInterpretation_RGB; + } + else if (p == "MONOCHROME1") + { + photometric_ = PhotometricInterpretation_Monochrome1; + } + else if (p == "MONOCHROME2") + { + photometric_ = PhotometricInterpretation_Monochrome2; + } + else if (p == "PALETTE COLOR") + { + photometric_ = PhotometricInterpretation_Palette; + } + else if (p == "HSV") + { + photometric_ = PhotometricInterpretation_HSV; + } + else if (p == "ARGB") + { + photometric_ = PhotometricInterpretation_ARGB; + } + else if (p == "CMYK") + { + photometric_ = PhotometricInterpretation_CMYK; + } + else if (p == "YBR_FULL") + { + photometric_ = PhotometricInterpretation_YBRFull; + } + else if (p == "YBR_FULL_422") + { + photometric_ = PhotometricInterpretation_YBRFull422; + } + else if (p == "YBR_PARTIAL_420") + { + photometric_ = PhotometricInterpretation_YBRPartial420; + } + else if (p == "YBR_PARTIAL_422") + { + photometric_ = PhotometricInterpretation_YBRPartial422; + } + else if (p == "YBR_ICT") + { + photometric_ = PhotometricInterpretation_YBR_ICT; + } + else if (p == "YBR_RCT") + { + photometric_ = PhotometricInterpretation_YBR_RCT; + } + else + { + photometric_ = PhotometricInterpretation_Unknown; + } + width_ = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_COLUMNS).AsString()); height_ = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_ROWS).AsString()); bitsAllocated_ = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_BITS_ALLOCATED).AsString()); @@ -159,30 +220,37 @@ bool DicomImageInformation::ExtractPixelFormat(PixelFormat& format) const { - if (GetBitsStored() == 8 && GetChannelCount() == 1 && !IsSigned()) + if (photometric_ == PhotometricInterpretation_Monochrome1 || + photometric_ == PhotometricInterpretation_Monochrome2) { - format = PixelFormat_Grayscale8; - return true; + if (GetBitsStored() == 8 && GetChannelCount() == 1 && !IsSigned()) + { + format = PixelFormat_Grayscale8; + return true; + } + + if (GetBitsAllocated() == 16 && GetChannelCount() == 1 && !IsSigned()) + { + format = PixelFormat_Grayscale16; + return true; + } + + if (GetBitsAllocated() == 16 && GetChannelCount() == 1 && IsSigned()) + { + format = PixelFormat_SignedGrayscale16; + return true; + } } - if (GetBitsStored() == 8 && GetChannelCount() == 3 && !IsSigned()) + if (GetBitsStored() == 8 && + GetChannelCount() == 3 && + !IsSigned() && + photometric_ == PhotometricInterpretation_RGB) { format = PixelFormat_RGB24; return true; } - if (GetBitsAllocated() == 16 && GetChannelCount() == 1 && !IsSigned()) - { - format = PixelFormat_Grayscale16; - return true; - } - - if (GetBitsAllocated() == 16 && GetChannelCount() == 1 && IsSigned()) - { - format = PixelFormat_SignedGrayscale16; - return true; - } - return false; } }
--- a/Core/DicomFormat/DicomImageInformation.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/DicomFormat/DicomImageInformation.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -54,6 +54,8 @@ unsigned int bitsStored_; unsigned int highBit_; + PhotometricInterpretation photometric_; + public: DicomImageInformation(const DicomMap& values); @@ -112,6 +114,11 @@ return highBit_ + 1 - bitsStored_; } + PhotometricInterpretation GetPhotometricInterpretation() const + { + return photometric_; + } + bool ExtractPixelFormat(PixelFormat& format) const; }; }
--- a/Core/DicomFormat/DicomInstanceHasher.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/DicomFormat/DicomInstanceHasher.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -48,8 +48,7 @@ seriesUid_ = seriesUid; instanceUid_ = instanceUid; - if (patientId_.size() == 0 || - studyUid_.size() == 0 || + if (studyUid_.size() == 0 || seriesUid_.size() == 0 || instanceUid_.size() == 0) { @@ -59,7 +58,9 @@ DicomInstanceHasher::DicomInstanceHasher(const DicomMap& instance) { - Setup(instance.GetValue(DICOM_TAG_PATIENT_ID).AsString(), + const DicomValue* patientId = instance.TestAndGetValue(DICOM_TAG_PATIENT_ID); + + Setup(patientId == NULL ? "" : patientId->AsString(), instance.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString(), instance.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).AsString(), instance.GetValue(DICOM_TAG_SOP_INSTANCE_UID).AsString());
--- a/Core/DicomFormat/DicomInstanceHasher.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/DicomFormat/DicomInstanceHasher.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/DicomFormat/DicomIntegerPixelAccessor.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/DicomFormat/DicomIntegerPixelAccessor.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/DicomFormat/DicomIntegerPixelAccessor.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/DicomFormat/DicomIntegerPixelAccessor.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -80,5 +80,10 @@ { return pixelData_; } + + size_t GetSize() const + { + return size_; + } }; }
--- a/Core/DicomFormat/DicomMap.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/DicomFormat/DicomMap.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -36,6 +36,7 @@ #include <stdio.h> #include <memory> #include "DicomString.h" +#include "DicomArray.h" #include "../OrthancException.h" @@ -190,6 +191,17 @@ } + void DicomMap::Assign(const DicomMap& other) + { + Clear(); + + for (Map::const_iterator it = other.map_.begin(); it != other.map_.end(); ++it) + { + map_.insert(std::make_pair(it->first, it->second->Clone())); + } + } + + const DicomValue& DicomMap::GetValue(const DicomTag& tag) const { const DicomValue* value = TestAndGetValue(tag); @@ -389,30 +401,21 @@ } - void DicomMap::ExtractMainDicomTagsForLevel(DicomMap& result, - ResourceType level) const + void DicomMap::Print(FILE* fp) const { - switch (level) - { - case ResourceType_Patient: - ExtractPatientInformation(result); - break; + DicomArray a(*this); + a.Print(fp); + } + - case ResourceType_Study: - ExtractStudyInformation(result); - break; + void DicomMap::GetTags(std::set<DicomTag>& tags) const + { + tags.clear(); - case ResourceType_Series: - ExtractSeriesInformation(result); - break; - - case ResourceType_Instance: - ExtractInstanceInformation(result); - break; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); + for (Map::const_iterator it = map_.begin(); + it != map_.end(); ++it) + { + tags.insert(it->first); } } - }
--- a/Core/DicomFormat/DicomMap.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/DicomFormat/DicomMap.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -78,8 +78,15 @@ Clear(); } + size_t GetSize() const + { + return map_.size(); + } + DicomMap* Clone() const; + void Assign(const DicomMap& other); + void Clear(); void SetValue(uint16_t group, @@ -125,11 +132,13 @@ const DicomValue& GetValue(const DicomTag& tag) const; + // DO NOT delete the returned value! const DicomValue* TestAndGetValue(uint16_t group, uint16_t element) const { return TestAndGetValue(DicomTag(group, element)); } + // DO NOT delete the returned value! const DicomValue* TestAndGetValue(const DicomTag& tag) const; void Remove(const DicomTag& tag); @@ -161,7 +170,8 @@ static void GetMainDicomTags(std::set<DicomTag>& result); - void ExtractMainDicomTagsForLevel(DicomMap& result, - ResourceType level) const; + void Print(FILE* fp) const; + + void GetTags(std::set<DicomTag>& tags) const; }; }
--- a/Core/DicomFormat/DicomNullValue.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/DicomFormat/DicomNullValue.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/DicomFormat/DicomString.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/DicomFormat/DicomString.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/DicomFormat/DicomTag.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/DicomFormat/DicomTag.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -116,4 +116,142 @@ return ""; } + + + void DicomTag::GetTagsForModule(std::set<DicomTag>& target, + DicomModule module) + { + // REFERENCE: 11_03pu.pdf, DICOM PS 3.3 2011 - Information Object Definitions + target.clear(); + + switch (module) + { + case DicomModule_Patient: + // This is Table C.7-1 "Patient Module Attributes" (p. 373) + target.insert(DicomTag(0x0010, 0x0010)); // Patient's name + target.insert(DicomTag(0x0010, 0x0020)); // Patient ID + target.insert(DicomTag(0x0010, 0x0030)); // Patient's birth date + target.insert(DicomTag(0x0010, 0x0040)); // Patient's sex + target.insert(DicomTag(0x0008, 0x1120)); // Referenced patient sequence + target.insert(DicomTag(0x0010, 0x0032)); // Patient's birth time + target.insert(DicomTag(0x0010, 0x1000)); // Other patient IDs + target.insert(DicomTag(0x0010, 0x1002)); // Other patient IDs sequence + target.insert(DicomTag(0x0010, 0x1001)); // Other patient names + target.insert(DicomTag(0x0010, 0x2160)); // Ethnic group + target.insert(DicomTag(0x0010, 0x4000)); // Patient comments + target.insert(DicomTag(0x0010, 0x2201)); // Patient species description + target.insert(DicomTag(0x0010, 0x2202)); // Patient species code sequence + target.insert(DicomTag(0x0010, 0x2292)); // Patient breed description + target.insert(DicomTag(0x0010, 0x2293)); // Patient breed code sequence + target.insert(DicomTag(0x0010, 0x2294)); // Breed registration sequence + target.insert(DicomTag(0x0010, 0x2297)); // Responsible person + target.insert(DicomTag(0x0010, 0x2298)); // Responsible person role + target.insert(DicomTag(0x0010, 0x2299)); // Responsible organization + target.insert(DicomTag(0x0012, 0x0062)); // Patient identity removed + target.insert(DicomTag(0x0012, 0x0063)); // De-identification method + target.insert(DicomTag(0x0012, 0x0064)); // De-identification method code sequence + + // Table 10-18 ISSUER OF PATIENT ID MACRO (p. 112) + target.insert(DicomTag(0x0010, 0x0021)); // Issuer of Patient ID + target.insert(DicomTag(0x0010, 0x0024)); // Issuer of Patient ID qualifiers sequence + break; + + case DicomModule_Study: + // This is Table C.7-3 "General Study Module Attributes" (p. 378) + target.insert(DicomTag(0x0020, 0x000d)); // Study instance UID + target.insert(DicomTag(0x0008, 0x0020)); // Study date + target.insert(DicomTag(0x0008, 0x0030)); // Study time + target.insert(DicomTag(0x0008, 0x0090)); // Referring physician's name + target.insert(DicomTag(0x0008, 0x0096)); // Referring physician identification sequence + target.insert(DicomTag(0x0020, 0x0010)); // Study ID + target.insert(DicomTag(0x0008, 0x0050)); // Accession number + target.insert(DicomTag(0x0008, 0x0051)); // Issuer of accession number sequence + target.insert(DicomTag(0x0008, 0x1030)); // Study description + target.insert(DicomTag(0x0008, 0x1048)); // Physician(s) of record + target.insert(DicomTag(0x0008, 0x1049)); // Physician(s) of record identification sequence + target.insert(DicomTag(0x0008, 0x1060)); // Name of physician(s) reading study + target.insert(DicomTag(0x0008, 0x1062)); // Physician(s) reading study identification sequence + target.insert(DicomTag(0x0032, 0x1034)); // Requesting service code sequence + target.insert(DicomTag(0x0008, 0x1110)); // Referenced study sequence + target.insert(DicomTag(0x0008, 0x1032)); // Procedure code sequence + target.insert(DicomTag(0x0040, 0x1012)); // Reason for performed procedure code sequence + break; + + case DicomModule_Series: + // This is Table C.7-5 "General Series Module Attributes" (p. 385) + target.insert(DicomTag(0x0008, 0x0060)); // Modality + target.insert(DicomTag(0x0020, 0x000e)); // Series Instance UID + target.insert(DicomTag(0x0020, 0x0011)); // Series Number + target.insert(DicomTag(0x0020, 0x0060)); // Laterality + target.insert(DicomTag(0x0008, 0x0021)); // Series Date + target.insert(DicomTag(0x0008, 0x0031)); // Series Time + target.insert(DicomTag(0x0008, 0x1050)); // Performing Physicians’ Name + target.insert(DicomTag(0x0008, 0x1052)); // Performing Physician Identification Sequence + target.insert(DicomTag(0x0018, 0x1030)); // Protocol Name + target.insert(DicomTag(0x0008, 0x103e)); // Series Description + target.insert(DicomTag(0x0008, 0x103f)); // Series Description Code Sequence + target.insert(DicomTag(0x0008, 0x1070)); // Operators' Name + target.insert(DicomTag(0x0008, 0x1072)); // Operator Identification Sequence + target.insert(DicomTag(0x0008, 0x1111)); // Referenced Performed Procedure Step Sequence + target.insert(DicomTag(0x0008, 0x1250)); // Related Series Sequence + target.insert(DicomTag(0x0018, 0x0015)); // Body Part Examined + target.insert(DicomTag(0x0018, 0x5100)); // Patient Position + target.insert(DicomTag(0x0028, 0x0108)); // Smallest Pixel Value in Series + target.insert(DicomTag(0x0029, 0x0109)); // Largest Pixel Value in Series + target.insert(DicomTag(0x0040, 0x0275)); // Request Attributes Sequence + target.insert(DicomTag(0x0010, 0x2210)); // Anatomical Orientation Type + + // Table 10-16 PERFORMED PROCEDURE STEP SUMMARY MACRO ATTRIBUTES + target.insert(DicomTag(0x0040, 0x0253)); // Performed Procedure Step ID + target.insert(DicomTag(0x0040, 0x0244)); // Performed Procedure Step Start Date + target.insert(DicomTag(0x0040, 0x0245)); // Performed Procedure Step Start Time + target.insert(DicomTag(0x0040, 0x0254)); // Performed Procedure Step Description + target.insert(DicomTag(0x0040, 0x0260)); // Performed Protocol Code Sequence + target.insert(DicomTag(0x0040, 0x0280)); // Comments on the Performed Procedure Step + break; + + case DicomModule_Instance: + // This is Table C.12-1 "SOP Common Module Attributes" (p. 1207) + target.insert(DicomTag(0x0008, 0x0016)); // SOP Class UID + target.insert(DicomTag(0x0008, 0x0018)); // SOP Instance UID + target.insert(DicomTag(0x0008, 0x0005)); // Specific Character Set + target.insert(DicomTag(0x0008, 0x0012)); // Instance Creation Date + target.insert(DicomTag(0x0008, 0x0013)); // Instance Creation Time + target.insert(DicomTag(0x0008, 0x0014)); // Instance Creator UID + target.insert(DicomTag(0x0008, 0x001a)); // Related General SOP Class UID + target.insert(DicomTag(0x0008, 0x001b)); // Original Specialized SOP Class UID + target.insert(DicomTag(0x0008, 0x0110)); // Coding Scheme Identification Sequence + target.insert(DicomTag(0x0008, 0x0201)); // Timezone Offset From UTC + target.insert(DicomTag(0x0018, 0xa001)); // Contributing Equipment Sequence + target.insert(DicomTag(0x0020, 0x0013)); // Instance Number + target.insert(DicomTag(0x0100, 0x0410)); // SOP Instance Status + target.insert(DicomTag(0x0100, 0x0420)); // SOP Authorization DateTime + target.insert(DicomTag(0x0100, 0x0424)); // SOP Authorization Comment + target.insert(DicomTag(0x0100, 0x0426)); // Authorization Equipment Certification Number + target.insert(DicomTag(0x0400, 0x0500)); // Encrypted Attributes Sequence + target.insert(DicomTag(0x0400, 0x0561)); // Original Attributes Sequence + target.insert(DicomTag(0x0040, 0xa390)); // HL7 Structured Document Reference Sequence + target.insert(DicomTag(0x0028, 0x0303)); // Longitudinal Temporal Information Modified + + // Table C.12-6 "DIGITAL SIGNATURES MACRO ATTRIBUTES" (p. 1216) + target.insert(DicomTag(0x4ffe, 0x0001)); // MAC Parameters sequence + target.insert(DicomTag(0xfffa, 0xfffa)); // Digital signatures sequence + break; + + // TODO IMAGE MODULE? + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + bool DicomTag::IsIdentifier() const + { + return (*this == DICOM_TAG_PATIENT_ID || + *this == DICOM_TAG_STUDY_INSTANCE_UID || + *this == DICOM_TAG_ACCESSION_NUMBER || + *this == DICOM_TAG_SERIES_INSTANCE_UID || + *this == DICOM_TAG_SOP_INSTANCE_UID); + } }
--- a/Core/DicomFormat/DicomTag.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/DicomFormat/DicomTag.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -33,8 +33,10 @@ #pragma once #include <string> +#include <set> #include <stdint.h> +#include "../Enumerations.h" namespace Orthanc { @@ -81,6 +83,11 @@ std::string Format() const; friend std::ostream& operator<< (std::ostream& o, const DicomTag& tag); + + static void GetTagsForModule(std::set<DicomTag>& target, + DicomModule module); + + bool IsIdentifier() const; }; // Aliases for the most useful tags @@ -104,10 +111,11 @@ static const DicomTag DICOM_TAG_PATIENT_NAME(0x0010, 0x0010); - // The following is used for "modify" operations + // The following is used for "modify/anonymize" operations static const DicomTag DICOM_TAG_SOP_CLASS_UID(0x0008, 0x0016); static const DicomTag DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID(0x0002, 0x0002); static const DicomTag DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID(0x0002, 0x0003); + static const DicomTag DICOM_TAG_DEIDENTIFICATION_METHOD(0x0012, 0x0063); // DICOM tags used for fMRI (thanks to Will Ryder) static const DicomTag DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS(0x0020, 0x0105);
--- a/Core/DicomFormat/DicomValue.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/DicomFormat/DicomValue.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/EnumerationDictionary.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/EnumerationDictionary.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -34,6 +34,7 @@ #include "OrthancException.h" +#include "Toolbox.h" #include <boost/lexical_cast.hpp> #include <string> #include <map> @@ -53,26 +54,23 @@ StringToEnumeration stringToEnumeration_; public: + void Clear() + { + enumerationToString_.clear(); + stringToEnumeration_.clear(); + } + void Add(Enumeration value, const std::string& str) { // Check if these values are free if (enumerationToString_.find(value) != enumerationToString_.end() || - stringToEnumeration_.find(str) != stringToEnumeration_.end()) + stringToEnumeration_.find(str) != stringToEnumeration_.end() || + Toolbox::IsInteger(str) /* Prevent the registration of a number */) { throw OrthancException(ErrorCode_BadRequest); } - // Prevent the registration of a number - try - { - boost::lexical_cast<int>(str); - throw OrthancException(ErrorCode_BadRequest); - } - catch (boost::bad_lexical_cast) - { - // OK, the string is not a number - } - + // OK, the string is free and is not a number enumerationToString_[value] = str; stringToEnumeration_[str] = value; stringToEnumeration_[boost::lexical_cast<std::string>(static_cast<int>(value))] = value;
--- a/Core/Enumerations.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/Enumerations.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -261,6 +261,197 @@ } + const char* EnumerationToString(Encoding encoding) + { + switch (encoding) + { + case Encoding_Ascii: + return "Ascii"; + + case Encoding_Utf8: + return "Utf8"; + + case Encoding_Latin1: + return "Latin1"; + + case Encoding_Latin2: + return "Latin2"; + + case Encoding_Latin3: + return "Latin3"; + + case Encoding_Latin4: + return "Latin4"; + + case Encoding_Latin5: + return "Latin5"; + + case Encoding_Cyrillic: + return "Cyrillic"; + + case Encoding_Windows1251: + return "Windows1251"; + + case Encoding_Arabic: + return "Arabic"; + + case Encoding_Greek: + return "Greek"; + + case Encoding_Hebrew: + return "Hebrew"; + + case Encoding_Thai: + return "Thai"; + + case Encoding_Japanese: + return "Japanese"; + + case Encoding_Chinese: + return "Chinese"; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + const char* EnumerationToString(PhotometricInterpretation photometric) + { + switch (photometric) + { + case PhotometricInterpretation_RGB: + return "RGB"; + + case PhotometricInterpretation_Monochrome1: + return "Monochrome1"; + + case PhotometricInterpretation_Monochrome2: + return "Monochrome2"; + + case PhotometricInterpretation_ARGB: + return "ARGB"; + + case PhotometricInterpretation_CMYK: + return "CMYK"; + + case PhotometricInterpretation_HSV: + return "HSV"; + + case PhotometricInterpretation_Palette: + return "Palette color"; + + case PhotometricInterpretation_YBRFull: + return "YBR full"; + + case PhotometricInterpretation_YBRFull422: + return "YBR full 422"; + + case PhotometricInterpretation_YBRPartial420: + return "YBR partial 420"; + + case PhotometricInterpretation_YBRPartial422: + return "YBR partial 422"; + + case PhotometricInterpretation_YBR_ICT: + return "YBR ICT"; + + case PhotometricInterpretation_YBR_RCT: + return "YBR RCT"; + + case PhotometricInterpretation_Unknown: + return "Unknown"; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + Encoding StringToEncoding(const char* encoding) + { + std::string s(encoding); + Toolbox::ToUpperCase(s); + + if (s == "UTF8") + { + return Encoding_Utf8; + } + + if (s == "ASCII") + { + return Encoding_Ascii; + } + + if (s == "LATIN1") + { + return Encoding_Latin1; + } + + if (s == "LATIN2") + { + return Encoding_Latin2; + } + + if (s == "LATIN3") + { + return Encoding_Latin3; + } + + if (s == "LATIN4") + { + return Encoding_Latin4; + } + + if (s == "LATIN5") + { + return Encoding_Latin5; + } + + if (s == "CYRILLIC") + { + return Encoding_Cyrillic; + } + + if (s == "WINDOWS1251") + { + return Encoding_Windows1251; + } + + if (s == "ARABIC") + { + return Encoding_Arabic; + } + + if (s == "GREEK") + { + return Encoding_Greek; + } + + if (s == "HEBREW") + { + return Encoding_Hebrew; + } + + if (s == "THAI") + { + return Encoding_Thai; + } + + if (s == "JAPANESE") + { + return Encoding_Japanese; + } + + if (s == "CHINESE") + { + return Encoding_Chinese; + } + + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + ResourceType StringToResourceType(const char* type) { std::string s(type); @@ -323,4 +514,115 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } } + + + bool GetDicomEncoding(Encoding& encoding, + const char* specificCharacterSet) + { + std::string s = specificCharacterSet; + Toolbox::ToUpperCase(s); + + // http://www.dabsoft.ch/dicom/3/C.12.1.1.2/ + // https://github.com/dcm4che/dcm4che/blob/master/dcm4che-core/src/main/java/org/dcm4che3/data/SpecificCharacterSet.java + if (s == "ISO_IR 6" || + s == "ISO_IR 192" || + s == "ISO 2022 IR 6") + { + encoding = Encoding_Utf8; + } + else if (s == "ISO_IR 100" || + s == "ISO 2022 IR 100") + { + encoding = Encoding_Latin1; + } + else if (s == "ISO_IR 101" || + s == "ISO 2022 IR 101") + { + encoding = Encoding_Latin2; + } + else if (s == "ISO_IR 109" || + s == "ISO 2022 IR 109") + { + encoding = Encoding_Latin3; + } + else if (s == "ISO_IR 110" || + s == "ISO 2022 IR 110") + { + encoding = Encoding_Latin4; + } + else if (s == "ISO_IR 148" || + s == "ISO 2022 IR 148") + { + encoding = Encoding_Latin5; + } + else if (s == "ISO_IR 144" || + s == "ISO 2022 IR 144") + { + encoding = Encoding_Cyrillic; + } + else if (s == "ISO_IR 127" || + s == "ISO 2022 IR 127") + { + encoding = Encoding_Arabic; + } + else if (s == "ISO_IR 126" || + s == "ISO 2022 IR 126") + { + encoding = Encoding_Greek; + } + else if (s == "ISO_IR 138" || + s == "ISO 2022 IR 138") + { + encoding = Encoding_Hebrew; + } + else if (s == "ISO_IR 166" || s == "ISO 2022 IR 166") + { + encoding = Encoding_Thai; + } + else if (s == "ISO_IR 13" || s == "ISO 2022 IR 13") + { + encoding = Encoding_Japanese; + } + else if (s == "GB18030") + { + encoding = Encoding_Chinese; + } + /* + else if (s == "ISO 2022 IR 149") + { + TODO + } + else if (s == "ISO 2022 IR 159") + { + TODO + } + else if (s == "ISO 2022 IR 87") + { + TODO + } + */ + else + { + return false; + } + + // The encoding was properly detected + return true; + } + + + const char* GetMimeType(FileContentType type) + { + switch (type) + { + case FileContentType_Dicom: + return "application/dicom"; + + case FileContentType_DicomAsJson: + return "application/json"; + + default: + return "application/octet-stream"; + } + } }
--- a/Core/Enumerations.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/Enumerations.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -57,6 +57,8 @@ ErrorCode_InexistentItem, ErrorCode_BadRequest, ErrorCode_NetworkProtocol, + ErrorCode_SystemCommand, + ErrorCode_Database, // Specific error codes ErrorCode_UriSyntax, @@ -71,7 +73,9 @@ ErrorCode_InexistentTag, ErrorCode_ReadOnly, ErrorCode_IncompatibleImageFormat, - ErrorCode_IncompatibleImageSize + ErrorCode_IncompatibleImageSize, + ErrorCode_SharedLibrary, + ErrorCode_Plugin }; /** @@ -228,10 +232,56 @@ }; + // http://www.dabsoft.ch/dicom/3/C.12.1.1.2/ enum Encoding { + Encoding_Ascii, Encoding_Utf8, - Encoding_Latin1 + Encoding_Latin1, + Encoding_Latin2, + Encoding_Latin3, + Encoding_Latin4, + Encoding_Latin5, // Turkish + Encoding_Cyrillic, + Encoding_Windows1251, // Windows-1251 (commonly used for Cyrillic) + Encoding_Arabic, + Encoding_Greek, + Encoding_Hebrew, + Encoding_Thai, // TIS 620-2533 + Encoding_Japanese, // JIS X 0201 (Shift JIS): Katakana + Encoding_Chinese // GB18030 - Chinese simplified + //Encoding_JapaneseKanji, // Multibyte - JIS X 0208: Kanji + //Encoding_JapaneseSupplementaryKanji, // Multibyte - JIS X 0212: Supplementary Kanji set + //Encoding_Korean, // Multibyte - KS X 1001: Hangul and Hanja + }; + + + // https://www.dabsoft.ch/dicom/3/C.7.6.3.1.2/ + enum PhotometricInterpretation + { + PhotometricInterpretation_ARGB, // Retired + PhotometricInterpretation_CMYK, // Retired + PhotometricInterpretation_HSV, // Retired + PhotometricInterpretation_Monochrome1, + PhotometricInterpretation_Monochrome2, + PhotometricInterpretation_Palette, + PhotometricInterpretation_RGB, + PhotometricInterpretation_YBRFull, + PhotometricInterpretation_YBRFull422, + PhotometricInterpretation_YBRPartial420, + PhotometricInterpretation_YBRPartial422, + PhotometricInterpretation_YBR_ICT, + PhotometricInterpretation_YBR_RCT, + PhotometricInterpretation_Unknown + }; + + enum DicomModule + { + DicomModule_Patient, + DicomModule_Study, + DicomModule_Series, + DicomModule_Instance, + DicomModule_Image }; @@ -249,6 +299,9 @@ enum FileContentType { + // If you add a value below, insert it in "PluginStorageArea" in + // the file "Plugins/Engine/OrthancPlugins.cpp" + FileContentType_Unknown = 0, FileContentType_Dicom = 1, FileContentType_DicomAsJson = 2, @@ -274,9 +327,20 @@ const char* EnumerationToString(ImageFormat format); + const char* EnumerationToString(Encoding encoding); + + const char* EnumerationToString(PhotometricInterpretation photometric); + + Encoding StringToEncoding(const char* encoding); + ResourceType StringToResourceType(const char* type); ImageFormat StringToImageFormat(const char* format); unsigned int GetBytesPerPixel(PixelFormat format); + + bool GetDicomEncoding(Encoding& encoding, + const char* specificCharacterSet); + + const char* GetMimeType(FileContentType type); }
--- a/Core/FileStorage/CompressedFileStorageAccessor.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/FileStorage/CompressedFileStorageAccessor.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -36,6 +36,10 @@ #include "../OrthancException.h" #include "FileStorageAccessor.h" #include "../HttpServer/BufferHttpSender.h" +#include "../Uuid.h" + +#include <memory> +#include <glog/logging.h> namespace Orthanc { @@ -43,6 +47,8 @@ size_t size, FileContentType type) { + std::string uuid = Toolbox::GenerateUuid(); + std::string md5; if (storeMD5_) @@ -54,7 +60,7 @@ { case CompressionType_None: { - std::string uuid = storage_.Create(data, size); + GetStorageArea().Create(uuid.c_str(), data, size, type); return FileInfo(uuid, type, size, md5); } @@ -70,7 +76,15 @@ Toolbox::ComputeMD5(compressedMD5, compressed); } - std::string uuid = storage_.Create(compressed); + if (compressed.size() > 0) + { + GetStorageArea().Create(uuid.c_str(), &compressed[0], compressed.size(), type); + } + else + { + GetStorageArea().Create(uuid.c_str(), NULL, 0, type); + } + return FileInfo(uuid, type, size, md5, CompressionType_Zlib, compressed.size(), compressedMD5); } @@ -80,25 +94,47 @@ } } - CompressedFileStorageAccessor::CompressedFileStorageAccessor(FileStorage& storage) : - storage_(storage) + + CompressedFileStorageAccessor::CompressedFileStorageAccessor() : + storage_(NULL), + compressionType_(CompressionType_None) { - compressionType_ = CompressionType_None; + } + + + CompressedFileStorageAccessor::CompressedFileStorageAccessor(IStorageArea& storage) : + storage_(&storage), + compressionType_(CompressionType_None) + { } + + IStorageArea& CompressedFileStorageAccessor::GetStorageArea() + { + if (storage_ == NULL) + { + LOG(ERROR) << "No storage area is currently available"; + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + return *storage_; + } + + void CompressedFileStorageAccessor::Read(std::string& content, - const std::string& uuid) + const std::string& uuid, + FileContentType type) { switch (compressionType_) { case CompressionType_None: - storage_.ReadFile(content, uuid); + GetStorageArea().Read(content, uuid, type); break; case CompressionType_Zlib: { std::string compressed; - storage_.ReadFile(compressed, uuid); + GetStorageArea().Read(compressed, uuid, type); zlib_.Uncompress(content, compressed); break; } @@ -108,20 +144,21 @@ } } - HttpFileSender* CompressedFileStorageAccessor::ConstructHttpFileSender(const std::string& uuid) + HttpFileSender* CompressedFileStorageAccessor::ConstructHttpFileSender(const std::string& uuid, + FileContentType type) { switch (compressionType_) { case CompressionType_None: { - FileStorageAccessor uncompressedAccessor(storage_); - return uncompressedAccessor.ConstructHttpFileSender(uuid); + FileStorageAccessor uncompressedAccessor(GetStorageArea()); + return uncompressedAccessor.ConstructHttpFileSender(uuid, type); } case CompressionType_Zlib: { std::string compressed; - storage_.ReadFile(compressed, uuid); + GetStorageArea().Read(compressed, uuid, type); std::auto_ptr<BufferHttpSender> sender(new BufferHttpSender); zlib_.Uncompress(sender->GetBuffer(), compressed); @@ -133,4 +170,11 @@ throw OrthancException(ErrorCode_NotImplemented); } } + + + void CompressedFileStorageAccessor::Remove(const std::string& uuid, + FileContentType type) + { + GetStorageArea().Remove(uuid, type); + } }
--- a/Core/FileStorage/CompressedFileStorageAccessor.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/FileStorage/CompressedFileStorageAccessor.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -32,8 +32,8 @@ #pragma once +#include "IStorageArea.h" #include "StorageAccessor.h" -#include "FileStorage.h" #include "../Compression/ZlibCompressor.h" namespace Orthanc @@ -41,7 +41,7 @@ class CompressedFileStorageAccessor : public StorageAccessor { private: - FileStorage& storage_; + IStorageArea* storage_; ZlibCompressor zlib_; CompressionType compressionType_; @@ -51,7 +51,21 @@ FileContentType type); public: - CompressedFileStorageAccessor(FileStorage& storage); + CompressedFileStorageAccessor(); + + CompressedFileStorageAccessor(IStorageArea& storage); + + void SetStorageArea(IStorageArea& storage) + { + storage_ = &storage; + } + + bool HasStorageArea() const + { + return storage_ != NULL; + } + + IStorageArea& GetStorageArea(); void SetCompressionForNextOperations(CompressionType compression) { @@ -64,8 +78,13 @@ } virtual void Read(std::string& content, - const std::string& uuid); + const std::string& uuid, + FileContentType type); - virtual HttpFileSender* ConstructHttpFileSender(const std::string& uuid); + virtual HttpFileSender* ConstructHttpFileSender(const std::string& uuid, + FileContentType type); + + virtual void Remove(const std::string& uuid, + FileContentType type); }; }
--- a/Core/FileStorage/FileInfo.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/FileStorage/FileInfo.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/FileStorage/FileStorage.cpp Wed Jun 25 15:34:40 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,309 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - **/ - - -#include "../PrecompiledHeaders.h" -#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) - { - //root_ = boost::filesystem::absolute(root).string(); - root_ = root; - - Toolbox::CreateDirectory(root); - } - - 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; - } -}
--- a/Core/FileStorage/FileStorage.h Wed Jun 25 15:34:40 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,96 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <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(); - } - }; -}
--- a/Core/FileStorage/FileStorageAccessor.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/FileStorage/FileStorageAccessor.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -33,6 +33,12 @@ #include "../PrecompiledHeaders.h" #include "FileStorageAccessor.h" +#include "../HttpServer/BufferHttpSender.h" +#include "../Uuid.h" + +#include <memory> +#include <stdio.h> + namespace Orthanc { FileInfo FileStorageAccessor::WriteInternal(const void* data, @@ -46,6 +52,21 @@ Toolbox::ComputeMD5(md5, data, size); } - return FileInfo(storage_.Create(data, size), type, size, md5); + std::string uuid = Toolbox::GenerateUuid(); + storage_.Create(uuid.c_str(), data, size, type); + + return FileInfo(uuid, type, size, md5); } + + + HttpFileSender* FileStorageAccessor::ConstructHttpFileSender(const std::string& uuid, + FileContentType type) + { + std::auto_ptr<BufferHttpSender> sender(new BufferHttpSender); + + storage_.Read(sender->GetBuffer(), uuid, type); + + return sender.release(); + } + }
--- a/Core/FileStorage/FileStorageAccessor.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/FileStorage/FileStorageAccessor.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -33,15 +33,14 @@ #pragma once #include "StorageAccessor.h" -#include "FileStorage.h" -#include "../HttpServer/FilesystemHttpSender.h" +#include "IStorageArea.h" namespace Orthanc { class FileStorageAccessor : public StorageAccessor { private: - FileStorage& storage_; + IStorageArea& storage_; protected: virtual FileInfo WriteInternal(const void* data, @@ -49,19 +48,24 @@ FileContentType type); public: - FileStorageAccessor(FileStorage& storage) : storage_(storage) + FileStorageAccessor(IStorageArea& storage) : storage_(storage) { } virtual void Read(std::string& content, - const std::string& uuid) + const std::string& uuid, + FileContentType type) { - storage_.ReadFile(content, uuid); + storage_.Read(content, uuid, type); } - virtual HttpFileSender* ConstructHttpFileSender(const std::string& uuid) + virtual HttpFileSender* ConstructHttpFileSender(const std::string& uuid, + FileContentType type); + + virtual void Remove(const std::string& uuid, + FileContentType type) { - return new FilesystemHttpSender(storage_.GetPath(uuid)); + storage_.Remove(uuid, type); } }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/FileStorage/FilesystemStorage.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,259 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "../PrecompiledHeaders.h" +#include "FilesystemStorage.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 FilesystemStorage::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; + } + + FilesystemStorage::FilesystemStorage(std::string root) + { + //root_ = boost::filesystem::absolute(root).string(); + root_ = root; + + Toolbox::CreateDirectory(root); + } + + void FilesystemStorage::Create(const std::string& uuid, + const void* content, + size_t size, + FileContentType /*type*/) + { + boost::filesystem::path path; + + path = GetPath(uuid); + + if (boost::filesystem::exists(path)) + { + // Extremely unlikely case: This Uuid has already been created + // in the past. + throw OrthancException(ErrorCode_InternalError); + } + + 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(); + } + + + void FilesystemStorage::Read(std::string& content, + const std::string& uuid, + FileContentType /*type*/) + { + content.clear(); + Toolbox::ReadFile(content, GetPath(uuid).string()); + } + + + uintmax_t FilesystemStorage::GetSize(const std::string& uuid) const + { + boost::filesystem::path path = GetPath(uuid); + return boost::filesystem::file_size(path); + } + + + + void FilesystemStorage::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 FilesystemStorage::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, FileContentType_Unknown /*ignored in this class*/); + } + } + + + void FilesystemStorage::Remove(const std::string& uuid, + FileContentType /*type*/) + { + 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 FilesystemStorage::GetCapacity() const + { + return boost::filesystem::space(root_).capacity; + } + + uintmax_t FilesystemStorage::GetAvailableSpace() const + { + return boost::filesystem::space(root_).available; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/FileStorage/FilesystemStorage.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,78 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "IStorageArea.h" + +#include <boost/filesystem.hpp> +#include <set> + +namespace Orthanc +{ + class FilesystemStorage : public IStorageArea + { + // TODO REMOVE THIS + friend class FilesystemHttpSender; + friend class FileStorageAccessor; + + private: + boost::filesystem::path root_; + + boost::filesystem::path GetPath(const std::string& uuid) const; + + public: + FilesystemStorage(std::string root); + + virtual void Create(const std::string& uuid, + const void* content, + size_t size, + FileContentType type); + + virtual void Read(std::string& content, + const std::string& uuid, + FileContentType type); + + virtual void Remove(const std::string& uuid, + FileContentType type); + + void ListAllFiles(std::set<std::string>& result) const; + + uintmax_t GetSize(const std::string& uuid) const; + + void Clear(); + + uintmax_t GetCapacity() const; + + uintmax_t GetAvailableSpace() const; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/FileStorage/IStorageArea.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,60 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "../Enumerations.h" + +#include <boost/noncopyable.hpp> + +namespace Orthanc +{ + class IStorageArea : public boost::noncopyable + { + public: + virtual ~IStorageArea() + { + } + + virtual void Create(const std::string& uuid, + const void* content, + size_t size, + FileContentType type) = 0; + + virtual void Read(std::string& content, + const std::string& uuid, + FileContentType type) = 0; + + virtual void Remove(const std::string& uuid, + FileContentType type) = 0; + }; +}
--- a/Core/FileStorage/StorageAccessor.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/FileStorage/StorageAccessor.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/FileStorage/StorageAccessor.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/FileStorage/StorageAccessor.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -85,8 +85,13 @@ FileContentType type); virtual void Read(std::string& content, - const std::string& uuid) = 0; + const std::string& uuid, + FileContentType type) = 0; - virtual HttpFileSender* ConstructHttpFileSender(const std::string& uuid) = 0; + virtual void Remove(const std::string& uuid, + FileContentType type) = 0; + + virtual HttpFileSender* ConstructHttpFileSender(const std::string& uuid, + FileContentType type) = 0; }; }
--- a/Core/HttpClient.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/HttpClient.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -109,6 +109,7 @@ method_ = HttpMethod_Get; lastStatus_ = HttpStatus_200_Ok; isVerbose_ = false; + timeout_ = 0; } @@ -161,13 +162,39 @@ answer.clear(); CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_URL, url_.c_str())); CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_WRITEDATA, &answer)); + + // Reset the parameters from previous calls to Apply() CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPHEADER, NULL)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPGET, 0L)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POST, 0L)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_NOBODY, 0L)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CUSTOMREQUEST, NULL)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDS, NULL)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, 0)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_PROXY, NULL)); + + // Set timeouts + if (timeout_ <= 0) + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_TIMEOUT, 10)); /* default: 10 seconds */ + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CONNECTTIMEOUT, 10)); /* default: 10 seconds */ + } + else + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_TIMEOUT, timeout_)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CONNECTTIMEOUT, timeout_)); + } if (credentials_.size() != 0) { CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_USERPWD, credentials_.c_str())); } + if (proxy_.size() != 0) + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_PROXY, proxy_.c_str())); + } + switch (method_) { case HttpMethod_Get: @@ -177,7 +204,31 @@ case HttpMethod_Post: CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POST, 1L)); CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPHEADER, pimpl_->postHeaders_)); + break; + case HttpMethod_Delete: + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_NOBODY, 1L)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CUSTOMREQUEST, "DELETE")); + break; + + case HttpMethod_Put: + // http://stackoverflow.com/a/7570281/881731: Don't use + // CURLOPT_PUT if there is a body + + // CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_PUT, 1L)); + + curl_easy_setopt(pimpl_->curl_, CURLOPT_CUSTOMREQUEST, "PUT"); /* !!! */ + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPHEADER, pimpl_->postHeaders_)); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + + if (method_ == HttpMethod_Post || + method_ == HttpMethod_Put) + { if (postData_.size() > 0) { CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDS, postData_.c_str())); @@ -188,21 +239,8 @@ CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDS, NULL)); CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, 0)); } - - break; - - case HttpMethod_Delete: - CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_NOBODY, 1L)); - CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CUSTOMREQUEST, "DELETE")); - break; + } - case HttpMethod_Put: - CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_PUT, 1L)); - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } // Do the actual request CheckCode(curl_easy_perform(pimpl_->curl_));
--- a/Core/HttpClient.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/HttpClient.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -52,6 +52,8 @@ HttpStatus lastStatus_; std::string postData_; bool isVerbose_; + long timeout_; + std::string proxy_; void Setup(); @@ -89,6 +91,21 @@ return method_; } + void SetTimeout(long seconds) + { + timeout_ = seconds; + } + + long GetTimeout() const + { + return timeout_; + } + + void SetPostData(const std::string& data) + { + postData_ = data; + } + std::string& AccessPostData() { return postData_; @@ -118,6 +135,11 @@ void SetCredentials(const char* username, const char* password); + void SetProxy(const std::string& proxy) + { + proxy_ = proxy; + } + static void GlobalInitialize(); static void GlobalFinalize();
--- a/Core/HttpServer/BufferHttpSender.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/HttpServer/BufferHttpSender.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -49,7 +49,9 @@ virtual bool SendData(HttpOutput& output) { if (buffer_.size()) - output.Send(&buffer_[0], buffer_.size()); + { + output.SendBody(&buffer_[0], buffer_.size()); + } return true; }
--- a/Core/HttpServer/EmbeddedResourceHttpHandler.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/HttpServer/EmbeddedResourceHttpHandler.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -51,24 +51,24 @@ } - bool EmbeddedResourceHttpHandler::IsServedUri(const UriComponents& uri) - { - return Toolbox::IsChildUri(baseUri_, uri); - } - - - void EmbeddedResourceHttpHandler::Handle( + bool EmbeddedResourceHttpHandler::Handle( HttpOutput& output, HttpMethod method, const UriComponents& uri, const Arguments& headers, - const Arguments& arguments, + const GetArguments& arguments, const std::string&) { + if (!Toolbox::IsChildUri(baseUri_, uri)) + { + // This URI is not served by this handler + return false; + } + if (method != HttpMethod_Get) { - output.SendMethodNotAllowedError("GET"); - return; + output.SendMethodNotAllowed("GET"); + return true; } std::string resourcePath = Toolbox::FlattenUri(uri, baseUri_.size()); @@ -78,12 +78,16 @@ { const void* buffer = EmbeddedResources::GetDirectoryResourceBuffer(resourceId_, resourcePath.c_str()); size_t size = EmbeddedResources::GetDirectoryResourceSize(resourceId_, resourcePath.c_str()); - output.AnswerBufferWithContentType(buffer, size, contentType); + + output.SetContentType(contentType.c_str()); + output.SendBody(buffer, size); } catch (OrthancException&) { LOG(WARNING) << "Unable to find HTTP resource: " << resourcePath; - output.SendHeader(HttpStatus_404_NotFound); + output.SendStatus(HttpStatus_404_NotFound); } + + return true; } }
--- a/Core/HttpServer/EmbeddedResourceHttpHandler.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/HttpServer/EmbeddedResourceHttpHandler.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -50,14 +50,12 @@ const std::string& baseUri, EmbeddedResources::DirectoryResourceId resourceId); - virtual bool IsServedUri(const UriComponents& uri); - - virtual void Handle( + virtual bool Handle( HttpOutput& output, HttpMethod method, const UriComponents& uri, const Arguments& headers, - const Arguments& arguments, + const GetArguments& arguments, const std::string&); }; }
--- a/Core/HttpServer/FilesystemHttpHandler.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/HttpServer/FilesystemHttpHandler.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -55,16 +55,18 @@ { namespace fs = boost::filesystem; - output.SendOkHeader("text/html", false, 0, NULL); - output.SendString("<html>"); - output.SendString(" <body>"); - output.SendString(" <h1>Subdirectories</h1>"); - output.SendString(" <ul>"); + output.SetContentType("text/html"); + + std::string s; + s += "<html>"; + s += " <body>"; + s += " <h1>Subdirectories</h1>"; + s += " <ul>"; if (uri.size() > 0) { std::string h = Toolbox::FlattenUri(uri) + "/.."; - output.SendString("<li><a href=\"" + h + "\">..</a></li>"); + s += "<li><a href=\"" + h + "\">..</a></li>"; } fs::directory_iterator end; @@ -78,12 +80,12 @@ std::string h = Toolbox::FlattenUri(uri) + "/" + f; if (fs::is_directory(it->status())) - output.SendString("<li><a href=\"" + h + "\">" + f + "</a></li>"); + s += "<li><a href=\"" + h + "\">" + f + "</a></li>"; } - output.SendString(" </ul>"); - output.SendString(" <h1>Files</h1>"); - output.SendString(" <ul>"); + s += " </ul>"; + s += " <h1>Files</h1>"; + s += " <ul>"; for (fs::directory_iterator it(p) ; it != end; ++it) { @@ -95,12 +97,14 @@ std::string h = Toolbox::FlattenUri(uri) + "/" + f; if (fs::is_regular_file(it->status())) - output.SendString("<li><a href=\"" + h + "\">" + f + "</a></li>"); + s += "<li><a href=\"" + h + "\">" + f + "</a></li>"; } - output.SendString(" </ul>"); - output.SendString(" </body>"); - output.SendString("</html>"); + s += " </ul>"; + s += " </body>"; + s += "</html>"; + + output.SendBody(s); } @@ -120,24 +124,24 @@ } - bool FilesystemHttpHandler::IsServedUri(const UriComponents& uri) - { - return Toolbox::IsChildUri(pimpl_->baseUri_, uri); - } - - - void FilesystemHttpHandler::Handle( + bool FilesystemHttpHandler::Handle( HttpOutput& output, HttpMethod method, const UriComponents& uri, const Arguments& headers, - const Arguments& arguments, + const GetArguments& arguments, const std::string&) { + if (!Toolbox::IsChildUri(pimpl_->baseUri_, uri)) + { + // This URI is not served by this handler + return false; + } + if (method != HttpMethod_Get) { - output.SendMethodNotAllowedError("GET"); - return; + output.SendMethodNotAllowed("GET"); + return true; } namespace fs = boost::filesystem; @@ -162,7 +166,9 @@ } else { - output.SendHeader(HttpStatus_404_NotFound); + output.SendStatus(HttpStatus_404_NotFound); } + + return true; } }
--- a/Core/HttpServer/FilesystemHttpHandler.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/HttpServer/FilesystemHttpHandler.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -52,14 +52,12 @@ FilesystemHttpHandler(const std::string& baseUri, const std::string& root); - virtual bool IsServedUri(const UriComponents& uri); - - virtual void Handle( + virtual bool Handle( HttpOutput& output, HttpMethod method, const UriComponents& uri, const Arguments& headers, - const Arguments& arguments, + const GetArguments& arguments, const std::string&); bool IsListDirectoryContent() const
--- a/Core/HttpServer/FilesystemHttpSender.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/HttpServer/FilesystemHttpSender.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -73,7 +73,7 @@ } else { - output.Send(&buffer[0], nbytes); + output.SendBody(&buffer[0], nbytes); } } @@ -94,7 +94,7 @@ Setup(); } - FilesystemHttpSender::FilesystemHttpSender(const FileStorage& storage, + FilesystemHttpSender::FilesystemHttpSender(const FilesystemStorage& storage, const std::string& uuid) { path_ = storage.GetPath(uuid).string();
--- a/Core/HttpServer/FilesystemHttpSender.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/HttpServer/FilesystemHttpSender.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -32,7 +32,7 @@ #pragma once #include "HttpFileSender.h" -#include "../FileStorage/FileStorage.h" +#include "../FileStorage/FilesystemStorage.h" namespace Orthanc { @@ -53,7 +53,7 @@ FilesystemHttpSender(const boost::filesystem::path& path); - FilesystemHttpSender(const FileStorage& storage, + FilesystemHttpSender(const FilesystemStorage& storage, const std::string& uuid); }; }
--- a/Core/HttpServer/HttpFileSender.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/HttpServer/HttpFileSender.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -33,13 +33,25 @@ #include "../PrecompiledHeaders.h" #include "HttpFileSender.h" +#include "../OrthancException.h" + #include <boost/lexical_cast.hpp> namespace Orthanc { void HttpFileSender::SendHeader(HttpOutput& output) { - output.SendOkHeader(contentType_.c_str(), true, GetFileSize(), downloadFilename_.c_str()); + if (contentType_.size() > 0) + { + output.SetContentType(contentType_.c_str()); + } + + if (downloadFilename_.size() > 0) + { + output.SetContentFilename(downloadFilename_.c_str()); + } + + output.SetContentLength(GetFileSize()); } void HttpFileSender::Send(HttpOutput& output) @@ -48,7 +60,8 @@ if (!SendData(output)) { - output.SendHeader(HttpStatus_500_InternalServerError); + throw OrthancException(ErrorCode_InternalError); + //output.SendHeader(HttpStatus_500_InternalServerError); } } }
--- a/Core/HttpServer/HttpFileSender.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/HttpServer/HttpFileSender.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/HttpServer/HttpHandler.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/HttpServer/HttpHandler.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -34,10 +34,12 @@ #include "HttpHandler.h" #include <string.h> +#include <iostream> + namespace Orthanc { - static void SplitGETNameValue(HttpHandler::Arguments& result, + static void SplitGETNameValue(HttpHandler::GetArguments& result, const char* start, const char* end) { @@ -58,11 +60,12 @@ Toolbox::UrlDecode(name); Toolbox::UrlDecode(value); - result.insert(std::make_pair(name, value)); + result.push_back(std::make_pair(name, value)); } - void HttpHandler::ParseGetQuery(HttpHandler::Arguments& result, const char* query) + void HttpHandler::ParseGetArguments(HttpHandler::GetArguments& result, + const char* query) { const char* pos = query; @@ -84,7 +87,25 @@ } + void HttpHandler::ParseGetQuery(UriComponents& uri, + HttpHandler::GetArguments& getArguments, + const char* query) + { + const char *questionMark = ::strchr(query, '?'); + if (questionMark == NULL) + { + // No question mark in the string + Toolbox::SplitUriComponents(uri, query); + getArguments.clear(); + } + else + { + Toolbox::SplitUriComponents(uri, std::string(query, questionMark)); + HttpHandler::ParseGetArguments(getArguments, questionMark + 1); + } + } + std::string HttpHandler::GetArgument(const Arguments& getArguments, const std::string& name, const std::string& defaultValue) @@ -101,6 +122,22 @@ } + std::string HttpHandler::GetArgument(const GetArguments& getArguments, + const std::string& name, + const std::string& defaultValue) + { + for (size_t i = 0; i < getArguments.size(); i++) + { + if (getArguments[i].first == name) + { + return getArguments[i].second; + } + } + + return defaultValue; + } + + void HttpHandler::ParseCookies(HttpHandler::Arguments& result, const HttpHandler::Arguments& httpHeaders) @@ -139,4 +176,16 @@ } } } + + + void HttpHandler::CompileGetArguments(Arguments& compiled, + const GetArguments& source) + { + compiled.clear(); + + for (size_t i = 0; i < source.size(); i++) + { + compiled[source[i].first] = source[i].second; + } + } }
--- a/Core/HttpServer/HttpHandler.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/HttpServer/HttpHandler.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -44,29 +44,39 @@ class HttpHandler { public: - typedef std::map<std::string, std::string> Arguments; + typedef std::map<std::string, std::string> Arguments; + typedef std::vector< std::pair<std::string, std::string> > GetArguments; virtual ~HttpHandler() { } - virtual bool IsServedUri(const UriComponents& uri) = 0; - - virtual void Handle(HttpOutput& output, + virtual bool Handle(HttpOutput& output, HttpMethod method, const UriComponents& uri, const Arguments& headers, - const Arguments& getArguments, + const GetArguments& getArguments, const std::string& postData) = 0; - static void ParseGetQuery(HttpHandler::Arguments& result, + static void ParseGetArguments(HttpHandler::GetArguments& result, + const char* query); + + static void ParseGetQuery(UriComponents& uri, + HttpHandler::GetArguments& getArguments, const char* query); static std::string GetArgument(const Arguments& getArguments, const std::string& name, const std::string& defaultValue); + static std::string GetArgument(const GetArguments& getArguments, + const std::string& name, + const std::string& defaultValue); + static void ParseCookies(HttpHandler::Arguments& result, const HttpHandler::Arguments& httpHeaders); + + static void CompileGetArguments(Arguments& compiled, + const GetArguments& source); }; }
--- a/Core/HttpServer/HttpOutput.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/HttpServer/HttpOutput.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -36,163 +36,238 @@ #include <iostream> #include <vector> #include <stdio.h> +#include <glog/logging.h> #include <boost/lexical_cast.hpp> #include "../OrthancException.h" #include "../Toolbox.h" namespace Orthanc { - void HttpOutput::SendString(const std::string& s) + HttpOutput::StateMachine::StateMachine(IHttpOutputStream& stream, + bool isKeepAlive) : + stream_(stream), + state_(State_WritingHeader), + status_(HttpStatus_200_Ok), + hasContentLength_(false), + contentPosition_(0), + keepAlive_(isKeepAlive) { - if (s.size() > 0) - { - Send(&s[0], s.size()); - } } - void HttpOutput::PrepareOkHeader(Header& header, - const char* contentType, - bool hasContentLength, - uint64_t contentLength, - const char* contentFilename) + HttpOutput::StateMachine::~StateMachine() { - header.clear(); - - if (contentType && contentType[0] != '\0') - { - header.push_back(std::make_pair("Content-Type", std::string(contentType))); - } - - if (hasContentLength) - { - header.push_back(std::make_pair("Content-Length", boost::lexical_cast<std::string>(contentLength))); - } - - if (contentFilename && contentFilename[0] != '\0') + if (state_ != State_Done) { - std::string attachment = "attachment; filename=\"" + std::string(contentFilename) + "\""; - header.push_back(std::make_pair("Content-Disposition", attachment)); - } - } - - void HttpOutput::SendOkHeader(const char* contentType, - bool hasContentLength, - uint64_t contentLength, - const char* contentFilename) - { - Header header; - PrepareOkHeader(header, contentType, hasContentLength, contentLength, contentFilename); - SendOkHeader(header); - } - - void HttpOutput::SendOkHeader(const Header& header) - { - std::string s = "HTTP/1.1 200 OK\r\n"; - - for (Header::const_iterator - it = header.begin(); it != header.end(); ++it) - { - s += it->first + ": " + it->second + "\r\n"; + //asm volatile ("int3;"); + //LOG(ERROR) << "This HTTP answer does not contain any body"; } - s += "\r\n"; - - Send(&s[0], s.size()); - } - - - void HttpOutput::SendMethodNotAllowedError(const std::string& allowed) - { - std::string s = - "HTTP/1.1 405 " + std::string(EnumerationToString(HttpStatus_405_MethodNotAllowed)) + - "\r\nAllow: " + allowed + - "\r\n\r\n"; - Send(&s[0], s.size()); - } - - - void HttpOutput::SendHeader(HttpStatus status) - { - if (status == HttpStatus_200_Ok || - status == HttpStatus_405_MethodNotAllowed) + if (hasContentLength_ && contentPosition_ != contentLength_) { - throw OrthancException("Please use the dedicated methods to this HTTP status code in HttpOutput"); - } - - SendHeaderInternal(status); - } - - - void HttpOutput::SendHeaderInternal(HttpStatus status) - { - std::string s = "HTTP/1.1 " + - boost::lexical_cast<std::string>(status) + - " " + std::string(EnumerationToString(status)) + - "\r\n\r\n"; - Send(&s[0], s.size()); - } - - - void HttpOutput::AnswerBufferWithContentType(const std::string& buffer, - const std::string& contentType) - { - SendOkHeader(contentType.c_str(), true, buffer.size(), NULL); - SendString(buffer); - } - - - void HttpOutput::PrepareCookies(Header& header, - const HttpHandler::Arguments& cookies) - { - for (HttpHandler::Arguments::const_iterator it = cookies.begin(); - it != cookies.end(); ++it) - { - header.push_back(std::make_pair("Set-Cookie", it->first + "=" + it->second)); + LOG(ERROR) << "This HTTP answer has not sent the proper number of bytes in its body"; } } - void HttpOutput::AnswerBufferWithContentType(const std::string& buffer, - const std::string& contentType, - const HttpHandler::Arguments& cookies) + void HttpOutput::StateMachine::SetHttpStatus(HttpStatus status) + { + if (state_ != State_WritingHeader) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + status_ = status; + } + + + void HttpOutput::StateMachine::SetContentLength(uint64_t length) { - Header header; - PrepareOkHeader(header, contentType.c_str(), true, buffer.size(), NULL); - PrepareCookies(header, cookies); - SendOkHeader(header); - SendString(buffer); + if (state_ != State_WritingHeader) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + hasContentLength_ = true; + contentLength_ = length; + } + + void HttpOutput::StateMachine::SetContentType(const char* contentType) + { + AddHeader("Content-Type", contentType); + } + + void HttpOutput::StateMachine::SetContentFilename(const char* filename) + { + // TODO Escape double quotes + AddHeader("Content-Disposition", "filename=\"" + std::string(filename) + "\""); + } + + void HttpOutput::StateMachine::SetCookie(const std::string& cookie, + const std::string& value) + { + if (state_ != State_WritingHeader) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + // TODO Escape "=" characters + AddHeader("Set-Cookie", cookie + "=" + value); } - void HttpOutput::AnswerBufferWithContentType(const void* buffer, - size_t size, - const std::string& contentType) + void HttpOutput::StateMachine::AddHeader(const std::string& header, + const std::string& value) + { + if (state_ != State_WritingHeader) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + headers_.push_back(header + ": " + value + "\r\n"); + } + + void HttpOutput::StateMachine::ClearHeaders() + { + if (state_ != State_WritingHeader) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + headers_.clear(); + } + + void HttpOutput::StateMachine::SendBody(const void* buffer, size_t length) { - SendOkHeader(contentType.c_str(), true, size, NULL); - Send(buffer, size); + if (state_ == State_Done) + { + if (length == 0) + { + return; + } + else + { + LOG(ERROR) << "Because of keep-alive connections, the entire body must be sent at once or Content-Length must be given"; + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + if (state_ == State_WritingHeader) + { + // Send the HTTP header before writing the body + + stream_.OnHttpStatusReceived(status_); + + std::string s = "HTTP/1.1 " + + boost::lexical_cast<std::string>(status_) + + " " + std::string(EnumerationToString(status_)) + + "\r\n"; + + if (keepAlive_) + { + s += "Connection: keep-alive\r\n"; + } + + for (std::list<std::string>::const_iterator + it = headers_.begin(); it != headers_.end(); ++it) + { + s += *it; + } + + if (status_ != HttpStatus_200_Ok) + { + hasContentLength_ = false; + } + + uint64_t contentLength = (hasContentLength_ ? contentLength_ : length); + s += "Content-Length: " + boost::lexical_cast<std::string>(contentLength) + "\r\n\r\n"; + + stream_.Send(true, s.c_str(), s.size()); + state_ = State_WritingBody; + } + + if (hasContentLength_ && + contentPosition_ + length > contentLength_) + { + LOG(ERROR) << "The body size exceeds what was declared with SetContentSize()"; + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + if (length > 0) + { + stream_.Send(false, buffer, length); + contentPosition_ += length; + } + + if (!hasContentLength_ || + contentPosition_ == contentLength_) + { + state_ = State_Done; + } } - void HttpOutput::AnswerBufferWithContentType(const void* buffer, - size_t size, - const std::string& contentType, - const HttpHandler::Arguments& cookies) + void HttpOutput::SendMethodNotAllowed(const std::string& allowed) { - Header header; - PrepareOkHeader(header, contentType.c_str(), true, size, NULL); - PrepareCookies(header, cookies); - SendOkHeader(header); - Send(buffer, size); + stateMachine_.ClearHeaders(); + stateMachine_.SetHttpStatus(HttpStatus_405_MethodNotAllowed); + stateMachine_.AddHeader("Allow", allowed); + stateMachine_.SendBody(NULL, 0); } + void HttpOutput::SendStatus(HttpStatus status) + { + if (status == HttpStatus_200_Ok || + status == HttpStatus_301_MovedPermanently || + status == HttpStatus_401_Unauthorized || + status == HttpStatus_405_MethodNotAllowed) + { + LOG(ERROR) << "Please use the dedicated methods to this HTTP status code in HttpOutput"; + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + stateMachine_.ClearHeaders(); + stateMachine_.SetHttpStatus(status); + stateMachine_.SendBody(NULL, 0); + } + void HttpOutput::Redirect(const std::string& path) { - std::string s = - "HTTP/1.1 301 " + std::string(EnumerationToString(HttpStatus_301_MovedPermanently)) + - "\r\nLocation: " + path + - "\r\n\r\n"; - Send(&s[0], s.size()); + stateMachine_.ClearHeaders(); + stateMachine_.SetHttpStatus(HttpStatus_301_MovedPermanently); + stateMachine_.AddHeader("Location", path); + stateMachine_.SendBody(NULL, 0); + } + + + void HttpOutput::SendUnauthorized(const std::string& realm) + { + stateMachine_.ClearHeaders(); + stateMachine_.SetHttpStatus(HttpStatus_401_Unauthorized); + stateMachine_.AddHeader("WWW-Authenticate", "Basic realm=\"" + realm + "\""); + stateMachine_.SendBody(NULL, 0); + } + + void HttpOutput::SendBody(const void* buffer, size_t length) + { + stateMachine_.SendBody(buffer, length); + } + + void HttpOutput::SendBody(const std::string& str) + { + if (str.size() == 0) + { + stateMachine_.SendBody(NULL, 0); + } + else + { + stateMachine_.SendBody(str.c_str(), str.size()); + } + } + + void HttpOutput::SendBody() + { + stateMachine_.SendBody(NULL, 0); } }
--- a/Core/HttpServer/HttpOutput.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/HttpServer/HttpOutput.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -36,64 +36,109 @@ #include <string> #include <stdint.h> #include "../Enumerations.h" +#include "IHttpOutputStream.h" #include "HttpHandler.h" namespace Orthanc { - class HttpOutput + class HttpOutput : public boost::noncopyable { private: typedef std::list< std::pair<std::string, std::string> > Header; - void SendHeaderInternal(HttpStatus status); + class StateMachine : public boost::noncopyable + { + private: + enum State + { + State_WritingHeader, + State_WritingBody, + State_Done + }; + + IHttpOutputStream& stream_; + State state_; + + HttpStatus status_; + bool hasContentLength_; + uint64_t contentLength_; + uint64_t contentPosition_; + bool keepAlive_; + std::list<std::string> headers_; - void PrepareOkHeader(Header& header, - const char* contentType, - bool hasContentLength, - uint64_t contentLength, - const char* contentFilename); + public: + StateMachine(IHttpOutputStream& stream, + bool isKeepAlive); + + ~StateMachine(); + + void SetHttpStatus(HttpStatus status); + + void SetContentLength(uint64_t length); + + void SetContentType(const char* contentType); - void SendOkHeader(const Header& header); + void SetContentFilename(const char* filename); + + void SetCookie(const std::string& cookie, + const std::string& value); - void PrepareCookies(Header& header, - const HttpHandler::Arguments& cookies); + void AddHeader(const std::string& header, + const std::string& value); + + void ClearHeaders(); + + void SendBody(const void* buffer, size_t length); + }; + + StateMachine stateMachine_; public: - virtual ~HttpOutput() + HttpOutput(IHttpOutputStream& stream, + bool isKeepAlive) : + stateMachine_(stream, isKeepAlive) { } - virtual void Send(const void* buffer, size_t length) = 0; + void SendStatus(HttpStatus status); + + void SetContentType(const char* contentType) + { + stateMachine_.SetContentType(contentType); + } + + void SetContentFilename(const char* filename) + { + stateMachine_.SetContentFilename(filename); + } + + void SetContentLength(uint64_t length) + { + stateMachine_.SetContentLength(length); + } - void SendOkHeader(const char* contentType, - bool hasContentLength, - uint64_t contentLength, - const char* contentFilename); + void SetCookie(const std::string& cookie, + const std::string& value) + { + stateMachine_.SetCookie(cookie, value); + } - void SendString(const std::string& s); + void AddHeader(const std::string& key, + const std::string& value) + { + stateMachine_.AddHeader(key, value); + } - void SendMethodNotAllowedError(const std::string& allowed); + void SendBody(const void* buffer, size_t length); + + void SendBody(const std::string& str); - void SendHeader(HttpStatus status); + void SendBody(); + + void SendMethodNotAllowed(const std::string& allowed); void Redirect(const std::string& path); - // Higher-level constructs to send entire buffers ---------------------------- - - void AnswerBufferWithContentType(const std::string& buffer, - const std::string& contentType); - - void AnswerBufferWithContentType(const std::string& buffer, - const std::string& contentType, - const HttpHandler::Arguments& cookies); - - void AnswerBufferWithContentType(const void* buffer, - size_t size, - const std::string& contentType); - - void AnswerBufferWithContentType(const void* buffer, - size_t size, - const std::string& contentType, - const HttpHandler::Arguments& cookies); + void SendUnauthorized(const std::string& realm); }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/HttpServer/IHttpOutputStream.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,53 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "../Enumerations.h" + +#include <string> +#include <boost/noncopyable.hpp> + +namespace Orthanc +{ + class IHttpOutputStream : public boost::noncopyable + { + public: + virtual ~IHttpOutputStream() + { + } + + virtual void OnHttpStatusReceived(HttpStatus status) = 0; + + virtual void Send(bool isHeader, const void* buffer, size_t length) = 0; + }; +}
--- a/Core/HttpServer/MongooseServer.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/HttpServer/MongooseServer.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -68,23 +68,28 @@ namespace { // Anonymous namespace to avoid clashes between compilation modules - class MongooseOutput : public HttpOutput + class MongooseOutputStream : public IHttpOutputStream { private: struct mg_connection* connection_; public: - MongooseOutput(struct mg_connection* connection) : connection_(connection) + MongooseOutputStream(struct mg_connection* connection) : connection_(connection) { } - virtual void Send(const void* buffer, size_t length) + virtual void Send(bool isHeader, const void* buffer, size_t length) { if (length > 0) { mg_write(connection_, buffer, length); } } + + virtual void OnHttpStatusReceived(HttpStatus status) + { + // Ignore this + } }; @@ -255,23 +260,6 @@ - HttpHandler* MongooseServer::FindHandler(const UriComponents& forUri) const - { - for (Handlers::const_iterator it = - handlers_.begin(); it != handlers_.end(); ++it) - { - if ((*it)->IsServedUri(forUri)) - { - return *it; - } - } - - return NULL; - } - - - - static PostDataStatus ReadBody(std::string& postData, struct mg_connection *connection, const HttpHandler::Arguments& headers) @@ -421,18 +409,8 @@ } - static void SendUnauthorized(HttpOutput& output) - { - std::string s = "HTTP/1.1 401 Unauthorized\r\n" - "WWW-Authenticate: Basic realm=\"" ORTHANC_REALM "\"" - "\r\n\r\n"; - output.Send(&s[0], s.size()); - } - - - static bool Authorize(const MongooseServer& that, - const HttpHandler::Arguments& headers, - HttpOutput& output) + static bool IsAccessGranted(const MongooseServer& that, + const HttpHandler::Arguments& headers) { bool granted = false; @@ -440,22 +418,15 @@ if (auth != headers.end()) { std::string s = auth->second; - if (s.substr(0, 6) == "Basic ") + if (s.size() > 6 && + s.substr(0, 6) == "Basic ") { std::string b64 = s.substr(6); granted = that.IsValidBasicHttpAuthentication(b64); } } - if (!granted) - { - SendUnauthorized(output); - return false; - } - else - { - return true; - } + return granted; } @@ -469,7 +440,8 @@ } std::string s = auth->second; - if (s.substr(0, 6) != "Basic ") + if (s.size() <= 6 || + s.substr(0, 6) != "Basic ") { return ""; } @@ -494,7 +466,7 @@ static bool ExtractMethod(HttpMethod& method, const struct mg_request_info *request, const HttpHandler::Arguments& headers, - const HttpHandler::Arguments& argumentsGET) + const HttpHandler::GetArguments& argumentsGET) { std::string overriden; @@ -512,10 +484,13 @@ { // 2. Faking with Ruby on Rail's approach // GET /my/resource?_method=delete <=> DELETE /my/resource - methodOverride = argumentsGET.find("_method"); - if (methodOverride != argumentsGET.end()) + for (size_t i = 0; i < argumentsGET.size(); i++) { - overriden = methodOverride->second; + if (argumentsGET[i].first == "_method") + { + overriden = argumentsGET[i].second; + break; + } } } @@ -568,179 +543,229 @@ } + static void InternalCallback(struct mg_connection *connection, + const struct mg_request_info *request) + { + MongooseServer* that = reinterpret_cast<MongooseServer*>(request->user_data); + MongooseOutputStream stream(connection); + HttpOutput output(stream, that->IsKeepAliveEnabled()); + // Check remote calls + if (!that->IsRemoteAccessAllowed() && + request->remote_ip != LOCALHOST) + { + output.SendUnauthorized(ORTHANC_REALM); + return; + } + + + // Extract the HTTP headers + HttpHandler::Arguments headers; + for (int i = 0; i < request->num_headers; i++) + { + std::string name = request->http_headers[i].name; + std::transform(name.begin(), name.end(), name.begin(), ::tolower); + headers.insert(std::make_pair(name, request->http_headers[i].value)); + } + + + // Extract the GET arguments + HttpHandler::GetArguments argumentsGET; + if (!strcmp(request->request_method, "GET")) + { + HttpHandler::ParseGetArguments(argumentsGET, request->query_string); + } + + + // Compute the HTTP method, taking method faking into consideration + HttpMethod method = HttpMethod_Get; + if (!ExtractMethod(method, request, headers, argumentsGET)) + { + output.SendStatus(HttpStatus_400_BadRequest); + return; + } + + + // Authenticate this connection + if (that->IsAuthenticationEnabled() && !IsAccessGranted(*that, headers)) + { + output.SendUnauthorized(ORTHANC_REALM); + return; + } + + + // Apply the filter, if it is installed + const IIncomingHttpRequestFilter *filter = that->GetIncomingHttpRequestFilter(); + if (filter != NULL) + { + std::string username = GetAuthenticatedUsername(headers); + + char remoteIp[24]; + sprintf(remoteIp, "%d.%d.%d.%d", + reinterpret_cast<const uint8_t*>(&request->remote_ip) [3], + reinterpret_cast<const uint8_t*>(&request->remote_ip) [2], + reinterpret_cast<const uint8_t*>(&request->remote_ip) [1], + reinterpret_cast<const uint8_t*>(&request->remote_ip) [0]); + + if (!filter->IsAllowed(method, request->uri, remoteIp, username.c_str())) + { + output.SendUnauthorized(ORTHANC_REALM); + return; + } + } + + + // Extract the body of the request for PUT and POST + std::string body; + if (method == HttpMethod_Post || + method == HttpMethod_Put) + { + PostDataStatus status; + + HttpHandler::Arguments::const_iterator ct = headers.find("content-type"); + if (ct == headers.end()) + { + // No content-type specified. Assume no multi-part content occurs at this point. + status = ReadBody(body, connection, headers); + } + else + { + std::string contentType = ct->second; + if (contentType.size() >= multipartLength && + !memcmp(contentType.c_str(), multipart, multipartLength)) + { + status = ParseMultipartPost(body, connection, headers, contentType, that->GetChunkStore()); + } + else + { + status = ReadBody(body, connection, headers); + } + } + + switch (status) + { + case PostDataStatus_NoLength: + output.SendStatus(HttpStatus_411_LengthRequired); + return; + + case PostDataStatus_Failure: + output.SendStatus(HttpStatus_400_BadRequest); + return; + + case PostDataStatus_Pending: + output.SendBody(); + return; + + default: + break; + } + } + + + // Decompose the URI into its components + UriComponents uri; + try + { + Toolbox::SplitUriComponents(uri, request->uri); + } + catch (OrthancException) + { + output.SendStatus(HttpStatus_400_BadRequest); + return; + } + + + // Loop over the candidate handlers for this URI + LOG(INFO) << EnumerationToString(method) << " " << Toolbox::FlattenUri(uri); + bool found = false; + + for (MongooseServer::Handlers::const_iterator it = + that->GetHandlers().begin(); it != that->GetHandlers().end() && !found; ++it) + { + try + { + found = (*it)->Handle(output, method, uri, headers, argumentsGET, body); + } + catch (OrthancException& e) + { + // Using this candidate handler results in an exception + LOG(ERROR) << "Exception in the HTTP handler: " << e.What(); + + switch (e.GetErrorCode()) + { + case ErrorCode_InexistentFile: + case ErrorCode_InexistentItem: + case ErrorCode_UnknownResource: + output.SendStatus(HttpStatus_404_NotFound); + break; + + case ErrorCode_BadRequest: + case ErrorCode_UriSyntax: + output.SendStatus(HttpStatus_400_BadRequest); + break; + + default: + output.SendStatus(HttpStatus_500_InternalServerError); + } + + return; + } + catch (boost::bad_lexical_cast&) + { + LOG(ERROR) << "Exception in the HTTP handler: Bad lexical cast"; + output.SendStatus(HttpStatus_400_BadRequest); + return; + } + catch (std::runtime_error&) + { + LOG(ERROR) << "Exception in the HTTP handler: Presumably a bad JSON request"; + output.SendStatus(HttpStatus_400_BadRequest); + return; + } + } + + if (!found) + { + output.SendStatus(HttpStatus_404_NotFound); + } + } + + +#if MONGOOSE_USE_CALLBACKS == 0 static void* Callback(enum mg_event event, struct mg_connection *connection, const struct mg_request_info *request) { if (event == MG_NEW_REQUEST) { - MongooseServer* that = reinterpret_cast<MongooseServer*>(request->user_data); - MongooseOutput output(connection); - - // Check remote calls - if (!that->IsRemoteAccessAllowed() && - request->remote_ip != LOCALHOST) - { - SendUnauthorized(output); - return (void*) ""; - } - - - // Extract the HTTP headers - HttpHandler::Arguments headers; - for (int i = 0; i < request->num_headers; i++) - { - std::string name = request->http_headers[i].name; - std::transform(name.begin(), name.end(), name.begin(), ::tolower); - headers.insert(std::make_pair(name, request->http_headers[i].value)); - } - - - // Extract the GET arguments - HttpHandler::Arguments argumentsGET; - if (!strcmp(request->request_method, "GET")) - { - HttpHandler::ParseGetQuery(argumentsGET, request->query_string); - } - - - // Compute the HTTP method, taking method faking into consideration - HttpMethod method; - if (!ExtractMethod(method, request, headers, argumentsGET)) - { - output.SendHeader(HttpStatus_400_BadRequest); - return (void*) ""; - } - - - // Authenticate this connection - if (that->IsAuthenticationEnabled() && - !Authorize(*that, headers, output)) - { - return (void*) ""; - } - - - // Apply the filter, if it is installed - const IIncomingHttpRequestFilter *filter = that->GetIncomingHttpRequestFilter(); - if (filter != NULL) - { - std::string username = GetAuthenticatedUsername(headers); - - char remoteIp[24]; - sprintf(remoteIp, "%d.%d.%d.%d", - reinterpret_cast<const uint8_t*>(&request->remote_ip) [3], - reinterpret_cast<const uint8_t*>(&request->remote_ip) [2], - reinterpret_cast<const uint8_t*>(&request->remote_ip) [1], - reinterpret_cast<const uint8_t*>(&request->remote_ip) [0]); - - if (!filter->IsAllowed(method, request->uri, remoteIp, username.c_str())) - { - SendUnauthorized(output); - return (void*) ""; - } - } - - - // Extract the body of the request for PUT and POST - std::string body; - if (method == HttpMethod_Post || - method == HttpMethod_Put) - { - PostDataStatus status; - - HttpHandler::Arguments::const_iterator ct = headers.find("content-type"); - if (ct == headers.end()) - { - // No content-type specified. Assume no multi-part content occurs at this point. - status = ReadBody(body, connection, headers); - } - else - { - std::string contentType = ct->second; - if (contentType.size() >= multipartLength && - !memcmp(contentType.c_str(), multipart, multipartLength)) - { - status = ParseMultipartPost(body, connection, headers, contentType, that->GetChunkStore()); - } - else - { - status = ReadBody(body, connection, headers); - } - } - - switch (status) - { - case PostDataStatus_NoLength: - output.SendHeader(HttpStatus_411_LengthRequired); - return (void*) ""; - - case PostDataStatus_Failure: - output.SendHeader(HttpStatus_400_BadRequest); - return (void*) ""; - - case PostDataStatus_Pending: - output.AnswerBufferWithContentType(NULL, 0, ""); - return (void*) ""; - - default: - break; - } - } - - - // Call the proper handler for this URI - UriComponents uri; - try - { - Toolbox::SplitUriComponents(uri, request->uri); - } - catch (OrthancException) - { - output.SendHeader(HttpStatus_400_BadRequest); - return (void*) ""; - } - - - HttpHandler* handler = that->FindHandler(uri); - if (handler) - { - try - { - LOG(INFO) << EnumerationToString(method) << " " << Toolbox::FlattenUri(uri); - handler->Handle(output, method, uri, headers, argumentsGET, body); - } - catch (OrthancException& e) - { - LOG(ERROR) << "MongooseServer Exception [" << e.What() << "]"; - output.SendHeader(HttpStatus_500_InternalServerError); - } - catch (boost::bad_lexical_cast&) - { - LOG(ERROR) << "MongooseServer Exception: Bad lexical cast"; - output.SendHeader(HttpStatus_400_BadRequest); - } - catch (std::runtime_error&) - { - LOG(ERROR) << "MongooseServer Exception: Presumably a bad JSON request"; - output.SendHeader(HttpStatus_400_BadRequest); - } - } - else - { - output.SendHeader(HttpStatus_404_NotFound); - } + InternalCallback(connection, request); // Mark as processed return (void*) ""; - } - else + } + else { return NULL; } } +#elif MONGOOSE_USE_CALLBACKS == 1 + static int Callback(struct mg_connection *connection) + { + struct mg_request_info *request = mg_get_request_info(connection); + + InternalCallback(connection, request); + + return 1; // Do not let Mongoose handle the request by itself + } + +#else +#error Please set MONGOOSE_USE_CALLBACKS +#endif + + + + bool MongooseServer::IsRunning() const { @@ -756,6 +781,7 @@ ssl_ = false; port_ = 8000; filter_ = NULL; + keepAlive_ = false; #if ORTHANC_SSL_ENABLED == 1 // Check for the Heartbleed exploit @@ -794,13 +820,32 @@ } const char *options[] = { + // Set the TCP port for the HTTP server "listening_ports", port.c_str(), + + // Optimization reported by Chris Hafey + // https://groups.google.com/d/msg/orthanc-users/CKueKX0pJ9E/_UCbl8T-VjIJ + "enable_keep_alive", (keepAlive_ ? "yes" : "no"), + + // Set the SSL certificate, if any. This must be the last option. ssl_ ? "ssl_certificate" : NULL, certificate_.c_str(), NULL }; +#if MONGOOSE_USE_CALLBACKS == 0 pimpl_->context_ = mg_start(&Callback, this, options); + +#elif MONGOOSE_USE_CALLBACKS == 1 + struct mg_callbacks callbacks; + memset(&callbacks, 0, sizeof(callbacks)); + callbacks.begin_request = Callback; + pimpl_->context_ = mg_start(&callbacks, this, options); + +#else +#error Please set MONGOOSE_USE_CALLBACKS +#endif + if (!pimpl_->context_) { throw OrthancException("Unable to launch the Mongoose server"); @@ -818,23 +863,17 @@ } - void MongooseServer::RegisterHandler(HttpHandler* handler) + void MongooseServer::RegisterHandler(HttpHandler& handler) { Stop(); - handlers_.push_back(handler); + handlers_.push_back(&handler); } void MongooseServer::ClearHandlers() { Stop(); - - for (Handlers::iterator it = - handlers_.begin(); it != handlers_.end(); ++it) - { - delete *it; - } } @@ -874,6 +913,15 @@ #endif } + + void MongooseServer::SetKeepAliveEnabled(bool enabled) + { + Stop(); + keepAlive_ = enabled; + LOG(WARNING) << "HTTP keep alive is " << (enabled ? "enabled" : "disabled"); + } + + void MongooseServer::SetAuthenticationEnabled(bool enabled) { Stop();
--- a/Core/HttpServer/MongooseServer.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/HttpServer/MongooseServer.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -59,12 +59,14 @@ class MongooseServer { + public: + typedef std::list<HttpHandler*> Handlers; + private: // http://stackoverflow.com/questions/311166/stdauto-ptr-or-boostshared-ptr-for-pimpl-idiom struct PImpl; boost::shared_ptr<PImpl> pimpl_; - typedef std::list<HttpHandler*> Handlers; Handlers handlers_; typedef std::set<std::string> RegisteredUsers; @@ -76,6 +78,7 @@ std::string certificate_; uint16_t port_; IIncomingHttpRequestFilter* filter_; + bool keepAlive_; bool IsRunning() const; @@ -100,7 +103,7 @@ void RegisterUser(const char* username, const char* password); - void RegisterHandler(HttpHandler* handler); // This takes the ownership + void RegisterHandler(HttpHandler& handler); bool IsAuthenticationEnabled() const { @@ -116,6 +119,13 @@ void SetSslEnabled(bool enabled); + bool IsKeepAliveEnabled() const + { + return keepAlive_; + } + + void SetKeepAliveEnabled(bool enabled); + const std::string& GetSslCertificate() const { return certificate_; @@ -139,11 +149,13 @@ void ClearHandlers(); - // Can return NULL if no handler is associated to this URI - HttpHandler* FindHandler(const UriComponents& forUri) const; - ChunkStore& GetChunkStore(); bool IsValidBasicHttpAuthentication(const std::string& basic) const; + + const Handlers& GetHandlers() const + { + return handlers_; + } }; }
--- a/Core/ICommand.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/ICommand.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/IDynamicObject.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/IDynamicObject.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/ImageFormats/ImageAccessor.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/ImageFormats/ImageAccessor.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -168,7 +168,7 @@ pitch_ = pitch; buffer_ = const_cast<void*>(buffer); - assert(GetBytesPerPixel(format_) * width_ <= pitch_); + assert(GetBytesPerPixel() * width_ <= pitch_); } @@ -185,7 +185,7 @@ pitch_ = pitch; buffer_ = buffer; - assert(GetBytesPerPixel(format_) * width_ <= pitch_); + assert(GetBytesPerPixel() * width_ <= pitch_); }
--- a/Core/ImageFormats/ImageAccessor.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/ImageFormats/ImageAccessor.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -62,6 +62,11 @@ return format_; } + unsigned int GetBytesPerPixel() const + { + return ::Orthanc::GetBytesPerPixel(format_); + } + unsigned int GetWidth() const { return width_;
--- a/Core/ImageFormats/ImageBuffer.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/ImageFormats/ImageBuffer.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/ImageFormats/ImageBuffer.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/ImageFormats/ImageBuffer.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/ImageFormats/ImageProcessing.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/ImageFormats/ImageProcessing.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -75,6 +75,44 @@ } + template <typename TargetType> + static void ConvertColorToGrayscale(ImageAccessor& target, + const ImageAccessor& source) + { + assert(source.GetFormat() == PixelFormat_RGB24); + + const TargetType minValue = std::numeric_limits<TargetType>::min(); + const TargetType maxValue = std::numeric_limits<TargetType>::max(); + + for (unsigned int y = 0; y < source.GetHeight(); y++) + { + TargetType* t = reinterpret_cast<TargetType*>(target.GetRow(y)); + const uint8_t* s = reinterpret_cast<const uint8_t*>(source.GetConstRow(y)); + + for (unsigned int x = 0; x < source.GetWidth(); x++, t++, s += 3) + { + // Y = 0.2126 R + 0.7152 G + 0.0722 B + int32_t v = (2126 * static_cast<int32_t>(s[0]) + + 7152 * static_cast<int32_t>(s[1]) + + 0722 * static_cast<int32_t>(s[2])) / 1000; + + if (static_cast<int32_t>(v) < static_cast<int32_t>(minValue)) + { + *t = minValue; + } + else if (static_cast<int32_t>(v) > static_cast<int32_t>(maxValue)) + { + *t = maxValue; + } + else + { + *t = static_cast<TargetType>(v); + } + } + } + } + + template <typename PixelType> static void SetInternal(ImageAccessor& image, int64_t constant) @@ -171,7 +209,7 @@ void MultiplyConstantInternal(ImageAccessor& image, float factor) { - if (abs(factor - 1.0f) <= std::numeric_limits<float>::epsilon()) + if (std::abs(factor - 1.0f) <= std::numeric_limits<float>::epsilon()) { return; } @@ -319,6 +357,27 @@ return; } + if (target.GetFormat() == PixelFormat_Grayscale8 && + source.GetFormat() == PixelFormat_RGB24) + { + ConvertColorToGrayscale<uint8_t>(target, source); + return; + } + + if (target.GetFormat() == PixelFormat_Grayscale16 && + source.GetFormat() == PixelFormat_RGB24) + { + ConvertColorToGrayscale<uint16_t>(target, source); + return; + } + + if (target.GetFormat() == PixelFormat_SignedGrayscale16 && + source.GetFormat() == PixelFormat_RGB24) + { + ConvertColorToGrayscale<int16_t>(target, source); + return; + } + throw OrthancException(ErrorCode_NotImplemented); }
--- a/Core/ImageFormats/ImageProcessing.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/ImageFormats/ImageProcessing.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/ImageFormats/PngReader.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/ImageFormats/PngReader.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/ImageFormats/PngReader.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/ImageFormats/PngReader.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/ImageFormats/PngWriter.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/ImageFormats/PngWriter.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/ImageFormats/PngWriter.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/ImageFormats/PngWriter.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/Lua/LuaContext.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/Lua/LuaContext.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -34,6 +34,7 @@ #include "LuaContext.h" #include <glog/logging.h> +#include <cassert> extern "C" { @@ -43,7 +44,7 @@ namespace Orthanc { - int LuaContext::PrintToLog(lua_State *state) + LuaContext& LuaContext::GetLuaContext(lua_State *state) { // Get the pointer to the "LuaContext" underlying object lua_getglobal(state, "_LuaContext"); @@ -52,6 +53,13 @@ assert(that != NULL); lua_pop(state, 1); + return *that; + } + + int LuaContext::PrintToLog(lua_State *state) + { + LuaContext& that = GetLuaContext(state); + // http://medek.wordpress.com/2009/02/03/wrapping-lua-errors-and-print-function/ int nArgs = lua_gettop(state); lua_getglobal(state, "tostring"); @@ -78,14 +86,243 @@ lua_pop(state, 1); } - LOG(INFO) << "Lua says: " << result; - that->log_.append(result); - that->log_.append("\n"); + LOG(WARNING) << "Lua says: " << result; + that.log_.append(result); + that.log_.append("\n"); + + return 0; + } + + + int LuaContext::SetHttpCredentials(lua_State *state) + { + LuaContext& that = GetLuaContext(state); + + // Check the types of the arguments + int nArgs = lua_gettop(state); + if (nArgs != 2 || + !lua_isstring(state, 1) || // Username + !lua_isstring(state, 2)) // Password + { + LOG(ERROR) << "Lua: Bad parameters to SetHttpCredentials()"; + } + else + { + // Configure the HTTP client + const char* username = lua_tostring(state, 1); + const char* password = lua_tostring(state, 2); + that.httpClient_.SetCredentials(username, password); + } return 0; } + bool LuaContext::AnswerHttpQuery(lua_State* state) + { + std::string str; + + try + { + httpClient_.Apply(str); + } + catch (OrthancException& e) + { + return false; + } + + // Return the result of the HTTP request + lua_pushstring(state, str.c_str()); + + return true; + } + + + int LuaContext::CallHttpGet(lua_State *state) + { + LuaContext& that = GetLuaContext(state); + + // Check the types of the arguments + int nArgs = lua_gettop(state); + if (nArgs != 1 || !lua_isstring(state, 1)) // URL + { + LOG(ERROR) << "Lua: Bad parameters to HttpGet()"; + lua_pushstring(state, "ERROR"); + return 1; + } + + // Configure the HTTP client class + const char* url = lua_tostring(state, 1); + that.httpClient_.SetMethod(HttpMethod_Get); + that.httpClient_.SetUrl(url); + + // Do the HTTP GET request + if (!that.AnswerHttpQuery(state)) + { + LOG(ERROR) << "Lua: Error in HttpGet() for URL " << url; + lua_pushstring(state, "ERROR"); + } + + return 1; + } + + + int LuaContext::CallHttpPostOrPut(lua_State *state, + HttpMethod method) + { + LuaContext& that = GetLuaContext(state); + + // Check the types of the arguments + int nArgs = lua_gettop(state); + if ((nArgs != 1 && nArgs != 2) || + !lua_isstring(state, 1) || // URL + (nArgs >= 2 && !lua_isstring(state, 2))) // Body data + { + LOG(ERROR) << "Lua: Bad parameters to HttpPost() or HttpPut()"; + lua_pushstring(state, "ERROR"); + return 1; + } + + // Configure the HTTP client class + const char* url = lua_tostring(state, 1); + that.httpClient_.SetMethod(method); + that.httpClient_.SetUrl(url); + + if (nArgs >= 2) + { + that.httpClient_.SetPostData(lua_tostring(state, 2)); + } + else + { + that.httpClient_.AccessPostData().clear(); + } + + // Do the HTTP POST/PUT request + if (!that.AnswerHttpQuery(state)) + { + LOG(ERROR) << "Lua: Error in HttpPost() or HttpPut() for URL " << url; + lua_pushstring(state, "ERROR"); + } + + return 1; + } + + + int LuaContext::CallHttpPost(lua_State *state) + { + return CallHttpPostOrPut(state, HttpMethod_Post); + } + + + int LuaContext::CallHttpPut(lua_State *state) + { + return CallHttpPostOrPut(state, HttpMethod_Put); + } + + + int LuaContext::CallHttpDelete(lua_State *state) + { + LuaContext& that = GetLuaContext(state); + + // Check the types of the arguments + int nArgs = lua_gettop(state); + if (nArgs != 1 || !lua_isstring(state, 1)) // URL + { + LOG(ERROR) << "Lua: Bad parameters to HttpDelete()"; + lua_pushstring(state, "ERROR"); + return 1; + } + + // Configure the HTTP client class + const char* url = lua_tostring(state, 1); + that.httpClient_.SetMethod(HttpMethod_Delete); + that.httpClient_.SetUrl(url); + + // Do the HTTP DELETE request + std::string s; + if (!that.httpClient_.Apply(s)) + { + LOG(ERROR) << "Lua: Error in HttpDelete() for URL " << url; + lua_pushstring(state, "ERROR"); + } + else + { + lua_pushstring(state, "SUCCESS"); + } + + return 1; + } + + + void LuaContext::PushJson(const Json::Value& value) + { + if (value.isString()) + { + lua_pushstring(lua_, value.asCString()); + } + else if (value.isDouble()) + { + lua_pushnumber(lua_, value.asDouble()); + } + else if (value.isInt()) + { + lua_pushinteger(lua_, value.asInt()); + } + else if (value.isUInt()) + { + lua_pushinteger(lua_, value.asUInt()); + } + else if (value.isBool()) + { + lua_pushboolean(lua_, value.asBool()); + } + else if (value.isNull()) + { + lua_pushnil(lua_); + } + else if (value.isArray()) + { + lua_newtable(lua_); + + // http://lua-users.org/wiki/SimpleLuaApiExample + for (Json::Value::ArrayIndex i = 0; i < value.size(); i++) + { + // Push the table index (note the "+1" because of Lua conventions) + lua_pushnumber(lua_, i + 1); + + // Push the value of the cell + PushJson(value[i]); + + // Stores the pair in the table + lua_rawset(lua_, -3); + } + } + else if (value.isObject()) + { + lua_newtable(lua_); + + Json::Value::Members members = value.getMemberNames(); + + for (Json::Value::Members::const_iterator + it = members.begin(); it != members.end(); ++it) + { + // Push the index of the cell + lua_pushstring(lua_, it->c_str()); + + // Push the value of the cell + PushJson(value[*it]); + + // Stores the pair in the table + lua_rawset(lua_, -3); + } + } + else + { + throw LuaException("Unsupported JSON conversion"); + } + } + + LuaContext::LuaContext() { lua_ = luaL_newstate(); @@ -96,6 +333,11 @@ luaL_openlibs(lua_); lua_register(lua_, "print", PrintToLog); + lua_register(lua_, "HttpGet", CallHttpGet); + lua_register(lua_, "HttpPost", CallHttpPost); + lua_register(lua_, "HttpPut", CallHttpPut); + lua_register(lua_, "HttpDelete", CallHttpDelete); + lua_register(lua_, "SetHttpCredentials", SetHttpCredentials); lua_pushlightuserdata(lua_, this); lua_setglobal(lua_, "_LuaContext"); @@ -108,11 +350,9 @@ } - void LuaContext::Execute(std::string* output, - const std::string& command) + void LuaContext::ExecuteInternal(std::string* output, + const std::string& command) { - boost::mutex::scoped_lock lock(mutex_); - log_.clear(); int error = (luaL_loadbuffer(lua_, command.c_str(), command.size(), "line") || lua_pcall(lua_, 0, 0, 0)); @@ -123,6 +363,7 @@ std::string description(lua_tostring(lua_, -1)); lua_pop(lua_, 1); /* pop error message from the stack */ + LOG(ERROR) << "Error while executing Lua script: " << description; throw LuaException(description); } @@ -137,15 +378,29 @@ { std::string command; EmbeddedResources::GetFileResource(command, resource); - Execute(command); + ExecuteInternal(NULL, command); } bool LuaContext::IsExistingFunction(const char* name) { - boost::mutex::scoped_lock lock(mutex_); lua_settop(lua_, 0); lua_getglobal(lua_, name); return lua_type(lua_, -1) == LUA_TFUNCTION; } + + + void LuaContext::Execute(Json::Value& output, + const std::string& command) + { + std::string s; + ExecuteInternal(&s, command); + + Json::Reader reader; + if (!reader.parse(s, output)) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + }
--- a/Core/Lua/LuaContext.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/Lua/LuaContext.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -33,8 +33,7 @@ #pragma once #include "LuaException.h" - -#include <boost/thread.hpp> +#include "../HttpClient.h" extern "C" { @@ -42,7 +41,7 @@ } #include <EmbeddedResources.h> - +#include <boost/noncopyable.hpp> namespace Orthanc { @@ -52,14 +51,29 @@ friend class LuaFunctionCall; lua_State *lua_; - boost::mutex mutex_; std::string log_; + HttpClient httpClient_; + + static LuaContext& GetLuaContext(lua_State *state); + + static int PrintToLog(lua_State *state); + + static int SetHttpCredentials(lua_State *state); - static int PrintToLog(lua_State *L); + static int CallHttpPostOrPut(lua_State *state, + HttpMethod method); + static int CallHttpGet(lua_State *state); + static int CallHttpPost(lua_State *state); + static int CallHttpPut(lua_State *state); + static int CallHttpDelete(lua_State *state); - void Execute(std::string* output, - const std::string& command); + bool AnswerHttpQuery(lua_State* state); + void ExecuteInternal(std::string* output, + const std::string& command); + + void PushJson(const Json::Value& value); + public: LuaContext(); @@ -67,17 +81,31 @@ void Execute(const std::string& command) { - Execute(NULL, command); + ExecuteInternal(NULL, command); } void Execute(std::string& output, const std::string& command) { - Execute(&output, command); + ExecuteInternal(&output, command); } + void Execute(Json::Value& output, + const std::string& command); + void Execute(EmbeddedResources::FileResourceId resource); bool IsExistingFunction(const char* name); + + void SetHttpCredentials(const char* username, + const char* password) + { + httpClient_.SetCredentials(username, password); + } + + void SetHttpProxy(const std::string& proxy) + { + httpClient_.SetProxy(proxy); + } }; }
--- a/Core/Lua/LuaException.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/Lua/LuaException.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/Lua/LuaFunctionCall.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/Lua/LuaFunctionCall.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -33,6 +33,10 @@ #include "../PrecompiledHeaders.h" #include "LuaFunctionCall.h" +#include <cassert> +#include <stdio.h> +#include <boost/lexical_cast.hpp> +#include <glog/logging.h> namespace Orthanc { @@ -47,7 +51,6 @@ LuaFunctionCall::LuaFunctionCall(LuaContext& context, const char* functionName) : context_(context), - lock_(context.mutex_), isExecuted_(false) { // Clear the stack to fulfill the invariant @@ -79,77 +82,13 @@ lua_pushnumber(context_.lua_, value); } - void LuaFunctionCall::PushJSON(const Json::Value& value) + void LuaFunctionCall::PushJson(const Json::Value& value) { CheckAlreadyExecuted(); - - if (value.isString()) - { - lua_pushstring(context_.lua_, value.asCString()); - } - else if (value.isDouble()) - { - lua_pushnumber(context_.lua_, value.asDouble()); - } - else if (value.isInt()) - { - lua_pushinteger(context_.lua_, value.asInt()); - } - else if (value.isUInt()) - { - lua_pushinteger(context_.lua_, value.asUInt()); - } - else if (value.isBool()) - { - lua_pushboolean(context_.lua_, value.asBool()); - } - else if (value.isNull()) - { - lua_pushnil(context_.lua_); - } - else if (value.isArray()) - { - lua_newtable(context_.lua_); - - // http://lua-users.org/wiki/SimpleLuaApiExample - for (Json::Value::ArrayIndex i = 0; i < value.size(); i++) - { - // Push the table index (note the "+1" because of Lua conventions) - lua_pushnumber(context_.lua_, i + 1); - - // Push the value of the cell - PushJSON(value[i]); - - // Stores the pair in the table - lua_rawset(context_.lua_, -3); - } - } - else if (value.isObject()) - { - lua_newtable(context_.lua_); - - Json::Value::Members members = value.getMemberNames(); - - for (Json::Value::Members::const_iterator - it = members.begin(); it != members.end(); ++it) - { - // Push the index of the cell - lua_pushstring(context_.lua_, it->c_str()); - - // Push the value of the cell - PushJSON(value[*it]); - - // Stores the pair in the table - lua_rawset(context_.lua_, -3); - } - } - else - { - throw LuaException("Unsupported JSON conversion"); - } + context_.PushJson(value); } - void LuaFunctionCall::Execute(int numOutputs) + void LuaFunctionCall::ExecuteInternal(int numOutputs) { CheckAlreadyExecuted(); @@ -176,13 +115,8 @@ bool LuaFunctionCall::ExecutePredicate() { - Execute(1); - - if (lua_gettop(context_.lua_) == 0) - { - throw LuaException("No output was provided by the function"); - } - + ExecuteInternal(1); + if (!lua_isboolean(context_.lua_, 1)) { throw LuaException("The function is not a predicate (only true/false outputs allowed)"); @@ -190,4 +124,99 @@ return lua_toboolean(context_.lua_, 1) != 0; } + + + static void PopJson(Json::Value& result, + lua_State* lua, + int top) + { + if (lua_istable(lua, top)) + { + Json::Value tmp = Json::objectValue; + bool isArray = true; + size_t size = 0; + + // http://stackoverflow.com/a/6142700/881731 + + // Push another reference to the table on top of the stack (so we know + // where it is, and this function can work for negative, positive and + // pseudo indices + lua_pushvalue(lua, top); + // stack now contains: -1 => table + lua_pushnil(lua); + // stack now contains: -1 => nil; -2 => table + while (lua_next(lua, -2)) + { + // stack now contains: -1 => value; -2 => key; -3 => table + // copy the key so that lua_tostring does not modify the original + lua_pushvalue(lua, -2); + // stack now contains: -1 => key; -2 => value; -3 => key; -4 => table + std::string key(lua_tostring(lua, -1)); + Json::Value v; + PopJson(v, lua, -2); + + tmp[key] = v; + + size += 1; + try + { + if (boost::lexical_cast<size_t>(key) != size) + { + isArray = false; + } + } + catch (boost::bad_lexical_cast&) + { + isArray = false; + } + + // pop value + copy of key, leaving original key + lua_pop(lua, 2); + // stack now contains: -1 => key; -2 => table + } + // stack now contains: -1 => table (when lua_next returns 0 it pops the key + // but does not push anything.) + // Pop table + lua_pop(lua, 1); + + // Stack is now the same as it was on entry to this function + + if (isArray) + { + result = Json::arrayValue; + for (size_t i = 0; i < size; i++) + { + result.append(tmp[boost::lexical_cast<std::string>(i + 1)]); + } + } + else + { + result = tmp; + } + } + else if (lua_isnumber(lua, top)) + { + result = static_cast<float>(lua_tonumber(lua, top)); + } + else if (lua_isstring(lua, top)) + { + result = std::string(lua_tostring(lua, top)); + } + else if (lua_isboolean(lua, top)) + { + result = static_cast<bool>(lua_toboolean(lua, top)); + } + else + { + LOG(WARNING) << "Unsupported Lua type when returning Json"; + result = Json::nullValue; + } + } + + + void LuaFunctionCall::ExecuteToJson(Json::Value& result) + { + ExecuteInternal(1); + PopJson(result, context_.lua_, lua_gettop(context_.lua_)); + } }
--- a/Core/Lua/LuaFunctionCall.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/Lua/LuaFunctionCall.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -36,18 +36,18 @@ #include <json/json.h> - namespace Orthanc { class LuaFunctionCall : public boost::noncopyable { private: LuaContext& context_; - boost::mutex::scoped_lock lock_; bool isExecuted_; void CheckAlreadyExecuted(); + void ExecuteInternal(int numOutputs); + public: LuaFunctionCall(LuaContext& context, const char* functionName); @@ -60,10 +60,15 @@ void PushDouble(double value); - void PushJSON(const Json::Value& value); + void PushJson(const Json::Value& value); - void Execute(int numOutputs = 0); + void Execute() + { + ExecuteInternal(0); + } bool ExecutePredicate(); + + void ExecuteToJson(Json::Value& result); }; }
--- a/Core/MultiThreading/ArrayFilledByThreads.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/MultiThreading/ArrayFilledByThreads.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/MultiThreading/BagOfRunnablesBySteps.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/MultiThreading/BagOfRunnablesBySteps.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/MultiThreading/BagOfRunnablesBySteps.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/MultiThreading/BagOfRunnablesBySteps.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/MultiThreading/ILockable.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/MultiThreading/ILockable.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/MultiThreading/IRunnableBySteps.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/MultiThreading/IRunnableBySteps.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/MultiThreading/Locker.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/MultiThreading/Locker.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/MultiThreading/Mutex.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/MultiThreading/Mutex.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -37,7 +37,7 @@ #if defined(_WIN32) #include <windows.h> -#elif defined(__linux) || defined(__FreeBSD_kernel__) || defined(__APPLE__) +#elif defined(__linux) || defined(__FreeBSD_kernel__) || defined(__APPLE__) || defined(__FreeBSD__) #include <pthread.h> #else #error Support your platform here @@ -75,7 +75,7 @@ } -#elif defined(__linux) || defined(__FreeBSD_kernel__) || defined(__APPLE__) +#elif defined(__linux) || defined(__FreeBSD_kernel__) || defined(__APPLE__) || defined(__FreeBSD__) struct Mutex::PImpl {
--- a/Core/MultiThreading/Mutex.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/MultiThreading/Mutex.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/MultiThreading/ReaderWriterLock.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/MultiThreading/ReaderWriterLock.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/MultiThreading/ReaderWriterLock.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/MultiThreading/ReaderWriterLock.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/MultiThreading/Semaphore.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,67 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "Semaphore.h" + +#include "../OrthancException.h" + + +namespace Orthanc +{ + Semaphore::Semaphore(unsigned int count) : count_(count) + { + if (count == 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + void Semaphore::Release() + { + boost::mutex::scoped_lock lock(mutex_); + + count_++; + condition_.notify_one(); + } + + void Semaphore::Acquire() + { + boost::mutex::scoped_lock lock(mutex_); + + while (count_ == 0) + { + condition_.wait(lock); + } + + count_++; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/MultiThreading/Semaphore.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,54 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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> +#include <boost/thread.hpp> + +namespace Orthanc +{ + class Semaphore : public boost::noncopyable + { + private: + unsigned int count_; + boost::mutex mutex_; + boost::condition_variable condition_; + + public: + explicit Semaphore(unsigned int count); + + void Release(); + + void Acquire(); + }; +}
--- a/Core/MultiThreading/SharedMessageQueue.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/MultiThreading/SharedMessageQueue.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -33,11 +33,40 @@ #include "../PrecompiledHeaders.h" #include "SharedMessageQueue.h" + + +/** + * FIFO (queue): + * + * back front + * +--+--+--+--+--+--+--+--+--+--+--+ + * Enqueue -> | | | | | | | | | | | | + * | | | | | | | | | | | | -> Dequeue + * +--+--+--+--+--+--+--+--+--+--+--+ + * ^ + * | + * Make room here + * + * + * LIFO (stack): + * + * back front + * +--+--+--+--+--+--+--+--+--+--+--+ + * | | | | | | | | | | | | <- Enqueue + * | | | | | | | | | | | | -> Dequeue + * +--+--+--+--+--+--+--+--+--+--+--+ + * ^ + * | + * Make room here + **/ + + namespace Orthanc { - SharedMessageQueue::SharedMessageQueue(unsigned int maxSize) + SharedMessageQueue::SharedMessageQueue(unsigned int maxSize) : + isFifo_(true), + maxSize_(maxSize) { - maxSize_ = maxSize; } @@ -56,12 +85,31 @@ if (maxSize_ != 0 && queue_.size() > maxSize_) { - // Too many elements in the queue: First remove the oldest - delete queue_.front(); - queue_.pop_front(); + if (isFifo_) + { + // Too many elements in the queue: Make room + delete queue_.front(); + queue_.pop_front(); + } + else + { + // Too many elements in the stack: Make room + delete queue_.back(); + queue_.pop_back(); + } } - queue_.push_back(message); + if (isFifo_) + { + // Queue policy (FIFO) + queue_.push_back(message); + } + else + { + // Stack policy (LIFO) + queue_.push_front(message); + } + elementAvailable_.notify_one(); } @@ -124,4 +172,17 @@ return true; } + + + void SharedMessageQueue::SetFifoPolicy() + { + boost::mutex::scoped_lock lock(mutex_); + isFifo_ = true; + } + + void SharedMessageQueue::SetLifoPolicy() + { + boost::mutex::scoped_lock lock(mutex_); + isFifo_ = false; + } }
--- a/Core/MultiThreading/SharedMessageQueue.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/MultiThreading/SharedMessageQueue.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -40,11 +40,12 @@ namespace Orthanc { - class SharedMessageQueue + class SharedMessageQueue : public boost::noncopyable { private: typedef std::list<IDynamicObject*> Queue; + bool isFifo_; unsigned int maxSize_; Queue queue_; boost::mutex mutex_; @@ -52,8 +53,8 @@ boost::condition_variable emptied_; public: - SharedMessageQueue(unsigned int maxSize = 0); - + explicit SharedMessageQueue(unsigned int maxSize = 0); + ~SharedMessageQueue(); // This transfers the ownership of the message @@ -63,5 +64,19 @@ IDynamicObject* Dequeue(int32_t millisecondsTimeout); bool WaitEmpty(int32_t millisecondsTimeout); + + bool IsFifoPolicy() const + { + return isFifo_; + } + + bool IsLifoPolicy() const + { + return !isFifo_; + } + + void SetFifoPolicy(); + + void SetLifoPolicy(); }; }
--- a/Core/MultiThreading/ThreadedCommandProcessor.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/MultiThreading/ThreadedCommandProcessor.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -175,7 +175,7 @@ { boost::mutex::scoped_lock lock(mutex_); - while (!remainingCommands_ == 0) + while (remainingCommands_ != 0) { processedCommand_.wait(lock); }
--- a/Core/MultiThreading/ThreadedCommandProcessor.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/MultiThreading/ThreadedCommandProcessor.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/OrthancException.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/OrthancException.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -121,6 +121,18 @@ case ErrorCode_IncompatibleImageFormat: return "Incompatible format of the images"; + case ErrorCode_SharedLibrary: + return "Error while using a shared library (plugin)"; + + case ErrorCode_SystemCommand: + return "Error while calling a system command"; + + case ErrorCode_Plugin: + return "Error encountered inside a plugin"; + + case ErrorCode_Database: + return "Error with the database engine"; + case ErrorCode_Custom: default: return "???";
--- a/Core/OrthancException.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/OrthancException.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/PrecompiledHeaders.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/PrecompiledHeaders.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/PrecompiledHeaders.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/PrecompiledHeaders.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -32,12 +32,12 @@ #pragma once -#if ORTHANC_USE_PRECOMPILED_HEADERS == 1 - -#ifndef NOMINMAX +#if defined(_WIN32) && !defined(NOMINMAX) #define NOMINMAX #endif +#if ORTHANC_USE_PRECOMPILED_HEADERS == 1 + #include <boost/date_time/posix_time/posix_time.hpp> #include <boost/filesystem.hpp> #include <boost/lexical_cast.hpp> @@ -49,6 +49,10 @@ #include <glog/logging.h> #include <json/value.h> +#if ORTHANC_PUGIXML_ENABLED == 1 +#include <pugixml.hpp> +#endif + #include "Enumerations.h" #include "OrthancException.h" #include "Toolbox.h"
--- a/Core/RestApi/RestApi.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/RestApi/RestApi.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -36,86 +36,87 @@ #include <stdlib.h> // To define "_exit()" under Windows #include <glog/logging.h> +#include <stdio.h> + namespace Orthanc { - bool RestApi::Call::ParseJsonRequestInternal(Json::Value& result, - const char* request) + namespace { - result.clear(); - Json::Reader reader; - return reader.parse(request, result); - } + // Anonymous namespace to avoid clashes between compilation modules + class HttpHandlerVisitor : public RestApiHierarchy::IVisitor + { + private: + RestApi& api_; + RestApiOutput& output_; + HttpMethod method_; + const HttpHandler::Arguments& headers_; + const HttpHandler::Arguments& getArguments_; + const std::string& postData_; + public: + HttpHandlerVisitor(RestApi& api, + RestApiOutput& output, + HttpMethod method, + const HttpHandler::Arguments& headers, + const HttpHandler::Arguments& getArguments, + const std::string& postData) : + api_(api), + output_(output), + method_(method), + headers_(headers), + getArguments_(getArguments), + postData_(postData) + { + } - bool RestApi::GetCall::ParseJsonRequest(Json::Value& result) const - { - result.clear(); + virtual bool Visit(const RestApiHierarchy::Resource& resource, + const UriComponents& uri, + const HttpHandler::Arguments& components, + const UriComponents& trailing) + { + if (resource.HasHandler(method_)) + { + switch (method_) + { + case HttpMethod_Get: + { + RestApiGetCall call(output_, api_, headers_, components, trailing, uri, getArguments_); + resource.Handle(call); + return true; + } + + case HttpMethod_Post: + { + RestApiPostCall call(output_, api_, headers_, components, trailing, uri, postData_); + resource.Handle(call); + return true; + } - for (HttpHandler::Arguments::const_iterator - it = getArguments_.begin(); it != getArguments_.end(); ++it) - { - result[it->first] = it->second; - } + case HttpMethod_Delete: + { + RestApiDeleteCall call(output_, api_, headers_, components, trailing, uri); + resource.Handle(call); + return true; + } - return true; + case HttpMethod_Put: + { + RestApiPutCall call(output_, api_, headers_, components, trailing, uri, postData_); + resource.Handle(call); + return true; + } + + default: + return false; + } + } + + return false; + } + }; } - 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) @@ -126,158 +127,128 @@ target = method; } - std::string RestApi::GetAcceptedMethods(const UriComponents& uri) + static std::string MethodsToString(const std::set<HttpMethod>& methods) { std::string s; - if (IsGetAccepted(uri)) + if (methods.find(HttpMethod_Get) != methods.end()) + { AddMethod(s, "GET"); + } - if (IsPutAccepted(uri)) - AddMethod(s, "PUT"); + if (methods.find(HttpMethod_Post) != methods.end()) + { + AddMethod(s, "POST"); + } - if (IsPostAccepted(uri)) - AddMethod(s, "POST"); + if (methods.find(HttpMethod_Put) != methods.end()) + { + AddMethod(s, "PUT"); + } - if (IsDeleteAccepted(uri)) + if (methods.find(HttpMethod_Delete) != methods.end()) + { 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, + bool RestApi::Handle(HttpOutput& output, HttpMethod method, const UriComponents& uri, const Arguments& headers, - const Arguments& getArguments, + const GetArguments& getArguments, const std::string& postData) { - bool ok = false; - RestApiOutput restOutput(output); - RestApiPath::Components components; - UriComponents trailing; + RestApiOutput wrappedOutput(output); - if (method == HttpMethod_Get) +#if ORTHANC_PUGIXML_ENABLED == 1 + // Look if the user wishes XML answers instead of JSON + // http://www.w3.org/Protocols/HTTP/HTRQ_Headers.html#z3 + Arguments::const_iterator it = headers.find("accept"); + if (it != headers.end()) { - for (GetHandlers::const_iterator it = getHandlers_.begin(); - it != getHandlers_.end(); ++it) + std::vector<std::string> accepted; + Toolbox::TokenizeString(accepted, it->second, ';'); + for (size_t i = 0; i < accepted.size(); i++) { - if (it->first->Match(components, trailing, uri)) + if (accepted[i] == "application/xml") { - //LOG(INFO) << "REST GET call on: " << Toolbox::FlattenUri(uri); - ok = true; - GetCall call(restOutput, *this, headers, components, trailing, uri, getArguments); - it->second(call); + wrappedOutput.SetConvertJsonToXml(true); + } + + if (accepted[i] == "application/json") + { + wrappedOutput.SetConvertJsonToXml(false); } } } - else if (method == HttpMethod_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(restOutput, *this, headers, components, trailing, uri, postData); - it->second(call); - } - } - } - else if (method == HttpMethod_Post) +#endif + + Arguments compiled; + HttpHandler::CompileGetArguments(compiled, getArguments); + + HttpHandlerVisitor visitor(*this, wrappedOutput, method, headers, compiled, postData); + + if (root_.LookupResource(uri, visitor)) { - 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(restOutput, *this, headers, components, trailing, uri, postData); - it->second(call); - } - } - } - else if (method == HttpMethod_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(restOutput, *this, headers, components, trailing, uri); - it->second(call); - } - } + wrappedOutput.Finalize(); + return true; } - if (!ok) + std::set<HttpMethod> methods; + root_.GetAcceptedMethods(methods, uri); + + if (methods.empty()) + { + return false; // This URI is not served by this REST API + } + else { LOG(INFO) << "REST method " << EnumerationToString(method) << " not allowed on: " << Toolbox::FlattenUri(uri); - output.SendMethodNotAllowedError(GetAcceptedMethods(uri)); + + output.SendMethodNotAllowed(MethodsToString(methods)); + + return true; } } void RestApi::Register(const std::string& path, - GetHandler handler) + RestApiGetCall::Handler handler) { - getHandlers_.push_back(std::make_pair(new RestApiPath(path), handler)); + root_.Register(path, handler); } void RestApi::Register(const std::string& path, - PutHandler handler) + RestApiPutCall::Handler handler) { - putHandlers_.push_back(std::make_pair(new RestApiPath(path), handler)); + root_.Register(path, handler); } void RestApi::Register(const std::string& path, - PostHandler handler) + RestApiPostCall::Handler handler) { - postHandlers_.push_back(std::make_pair(new RestApiPath(path), handler)); + root_.Register(path, handler); } void RestApi::Register(const std::string& path, - DeleteHandler handler) + RestApiDeleteCall::Handler handler) + { + root_.Register(path, handler); + } + + void RestApi::AutoListChildren(RestApiGetCall& call) { - deleteHandlers_.push_back(std::make_pair(new RestApiPath(path), handler)); + RestApi& context = call.GetContext(); + + Json::Value directory; + if (context.root_.GetDirectory(directory, call.GetFullUri())) + { + call.GetOutput().AnswerJson(directory); + } } }
--- a/Core/RestApi/RestApi.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/RestApi/RestApi.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -32,9 +32,7 @@ #pragma once -#include "../HttpServer/HttpHandler.h" -#include "RestApiPath.h" -#include "RestApiOutput.h" +#include "RestApiHierarchy.h" #include <list> @@ -42,254 +40,29 @@ { class RestApi : public HttpHandler { - public: - class Call - { - friend class RestApi; - - private: - RestApiOutput& output_; - RestApi& context_; - const HttpHandler::Arguments& httpHeaders_; - const RestApiPath::Components& uriComponents_; - const UriComponents& trailing_; - const UriComponents& fullUri_; - - Call(RestApiOutput& output, - RestApi& context, - const HttpHandler::Arguments& httpHeaders, - const RestApiPath::Components& uriComponents, - const UriComponents& trailing, - const UriComponents& fullUri) : - output_(output), - context_(context), - httpHeaders_(httpHeaders), - uriComponents_(uriComponents), - trailing_(trailing), - fullUri_(fullUri) - { - } - - protected: - static bool ParseJsonRequestInternal(Json::Value& result, - const char* request); - - 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); - } - - const HttpHandler::Arguments& GetHttpHeaders() const - { - return httpHeaders_; - } - - void ParseCookies(HttpHandler::Arguments& result) const - { - HttpHandler::ParseCookies(result, httpHeaders_); - } - - virtual bool ParseJsonRequest(Json::Value& result) const = 0; - }; - - - class GetCall : public Call - { - friend class RestApi; - - private: - const HttpHandler::Arguments& getArguments_; - - public: - GetCall(RestApiOutput& output, - RestApi& context, - const HttpHandler::Arguments& httpHeaders, - const RestApiPath::Components& uriComponents, - const UriComponents& trailing, - const UriComponents& fullUri, - const HttpHandler::Arguments& getArguments) : - Call(output, context, httpHeaders, uriComponents, trailing, fullUri), - getArguments_(getArguments) - { - } - - 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(); - } - - virtual bool ParseJsonRequest(Json::Value& result) const; - }; - - - class PutCall : public Call - { - friend class RestApi; - - private: - const std::string& data_; - - public: - PutCall(RestApiOutput& output, - RestApi& context, - const HttpHandler::Arguments& httpHeaders, - const RestApiPath::Components& uriComponents, - const UriComponents& trailing, - const UriComponents& fullUri, - const std::string& data) : - Call(output, context, httpHeaders, uriComponents, trailing, fullUri), - data_(data) - { - } - - const std::string& GetPutBody() const - { - return data_; - } - - virtual bool ParseJsonRequest(Json::Value& result) const - { - return ParseJsonRequestInternal(result, GetPutBody().c_str()); - } - }; - - class PostCall : public Call - { - friend class RestApi; - - private: - const std::string& data_; - - public: - PostCall(RestApiOutput& output, - RestApi& context, - const HttpHandler::Arguments& httpHeaders, - const RestApiPath::Components& uriComponents, - const UriComponents& trailing, - const UriComponents& fullUri, - const std::string& data) : - Call(output, context, httpHeaders, uriComponents, trailing, fullUri), - data_(data) - { - } - - const std::string& GetPostBody() const - { - return data_; - } - - virtual bool ParseJsonRequest(Json::Value& result) const - { - return ParseJsonRequestInternal(result, GetPostBody().c_str()); - } - }; - - class DeleteCall : public Call - { - public: - DeleteCall(RestApiOutput& output, - RestApi& context, - const HttpHandler::Arguments& httpHeaders, - const RestApiPath::Components& uriComponents, - const UriComponents& trailing, - const UriComponents& fullUri) : - Call(output, context, httpHeaders, uriComponents, trailing, fullUri) - { - } - - virtual bool ParseJsonRequest(Json::Value& result) const - { - result.clear(); - return true; - } - }; - - 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); + RestApiHierarchy root_; public: - RestApi() - { - } + static void AutoListChildren(RestApiGetCall& call); - ~RestApi(); - - virtual bool IsServedUri(const UriComponents& uri); - - virtual void Handle(HttpOutput& output, + virtual bool Handle(HttpOutput& output, HttpMethod method, const UriComponents& uri, const Arguments& headers, - const Arguments& getArguments, + const GetArguments& getArguments, const std::string& postData); void Register(const std::string& path, - GetHandler handler); + RestApiGetCall::Handler handler); void Register(const std::string& path, - PutHandler handler); + RestApiPutCall::Handler handler); void Register(const std::string& path, - PostHandler handler); + RestApiPostCall::Handler handler); void Register(const std::string& path, - DeleteHandler handler); + RestApiDeleteCall::Handler handler); }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/RestApi/RestApiCall.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,44 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "RestApiCall.h" + +namespace Orthanc +{ + bool RestApiCall::ParseJsonRequestInternal(Json::Value& result, + const char* request) + { + result.clear(); + Json::Reader reader; + return reader.parse(request, result); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/RestApi/RestApiCall.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,119 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 <boost/noncopyable.hpp> + +namespace Orthanc +{ + class RestApi; + + class RestApiCall : public boost::noncopyable + { + private: + RestApiOutput& output_; + RestApi& context_; + const HttpHandler::Arguments& httpHeaders_; + const HttpHandler::Arguments& uriComponents_; + const UriComponents& trailing_; + const UriComponents& fullUri_; + + protected: + static bool ParseJsonRequestInternal(Json::Value& result, + const char* request); + + public: + RestApiCall(RestApiOutput& output, + RestApi& context, + const HttpHandler::Arguments& httpHeaders, + const HttpHandler::Arguments& uriComponents, + const UriComponents& trailing, + const UriComponents& fullUri) : + output_(output), + context_(context), + httpHeaders_(httpHeaders), + uriComponents_(uriComponents), + trailing_(trailing), + fullUri_(fullUri) + { + } + + 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); + } + + const HttpHandler::Arguments& GetHttpHeaders() const + { + return httpHeaders_; + } + + void ParseCookies(HttpHandler::Arguments& result) const + { + HttpHandler::ParseCookies(result, httpHeaders_); + } + + virtual bool ParseJsonRequest(Json::Value& result) const = 0; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/RestApi/RestApiDeleteCall.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,60 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "RestApiCall.h" + +namespace Orthanc +{ + class RestApiDeleteCall : public RestApiCall + { + public: + typedef void (*Handler) (RestApiDeleteCall& call); + + RestApiDeleteCall(RestApiOutput& output, + RestApi& context, + const HttpHandler::Arguments& httpHeaders, + const HttpHandler::Arguments& uriComponents, + const UriComponents& trailing, + const UriComponents& fullUri) : + RestApiCall(output, context, httpHeaders, uriComponents, trailing, fullUri) + { + } + + virtual bool ParseJsonRequest(Json::Value& result) const + { + result.clear(); + return true; + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/RestApi/RestApiGetCall.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,49 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "RestApiGetCall.h" + +namespace Orthanc +{ + bool RestApiGetCall::ParseJsonRequest(Json::Value& result) const + { + result.clear(); + + for (HttpHandler::Arguments::const_iterator + it = getArguments_.begin(); it != getArguments_.end(); ++it) + { + result[it->first] = it->second; + } + + return true; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/RestApi/RestApiGetCall.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,72 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "RestApiCall.h" + +namespace Orthanc +{ + class RestApiGetCall : public RestApiCall + { + private: + const HttpHandler::Arguments& getArguments_; + + public: + typedef void (*Handler) (RestApiGetCall& call); + + RestApiGetCall(RestApiOutput& output, + RestApi& context, + const HttpHandler::Arguments& httpHeaders, + const HttpHandler::Arguments& uriComponents, + const UriComponents& trailing, + const UriComponents& fullUri, + const HttpHandler::Arguments& getArguments) : + RestApiCall(output, context, httpHeaders, uriComponents, trailing, fullUri), + getArguments_(getArguments) + { + } + + 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(); + } + + virtual bool ParseJsonRequest(Json::Value& result) const; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/RestApi/RestApiHierarchy.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,473 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "RestApiHierarchy.h" + +#include "../OrthancException.h" + +#include <cassert> +#include <stdio.h> + +namespace Orthanc +{ + RestApiHierarchy::Resource::Resource() : + getHandler_(NULL), + postHandler_(NULL), + putHandler_(NULL), + deleteHandler_(NULL) + { + } + + + bool RestApiHierarchy::Resource::HasHandler(HttpMethod method) const + { + switch (method) + { + case HttpMethod_Get: + return getHandler_ != NULL; + + case HttpMethod_Post: + return postHandler_ != NULL; + + case HttpMethod_Put: + return putHandler_ != NULL; + + case HttpMethod_Delete: + return deleteHandler_ != NULL; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + bool RestApiHierarchy::Resource::IsEmpty() const + { + return (getHandler_ == NULL && + postHandler_ == NULL && + putHandler_ == NULL && + deleteHandler_ == NULL); + } + + + RestApiHierarchy& RestApiHierarchy::AddChild(Children& children, + const std::string& name) + { + Children::iterator it = children.find(name); + + if (it == children.end()) + { + // Create new child + RestApiHierarchy *child = new RestApiHierarchy; + children[name] = child; + return *child; + } + else + { + return *it->second; + } + } + + + + bool RestApiHierarchy::Resource::Handle(RestApiGetCall& call) const + { + if (getHandler_ != NULL) + { + getHandler_(call); + return true; + } + else + { + return false; + } + } + + + bool RestApiHierarchy::Resource::Handle(RestApiPutCall& call) const + { + if (putHandler_ != NULL) + { + putHandler_(call); + return true; + } + else + { + return false; + } + } + + + bool RestApiHierarchy::Resource::Handle(RestApiPostCall& call) const + { + if (postHandler_ != NULL) + { + postHandler_(call); + return true; + } + else + { + return false; + } + } + + + bool RestApiHierarchy::Resource::Handle(RestApiDeleteCall& call) const + { + if (deleteHandler_ != NULL) + { + deleteHandler_(call); + return true; + } + else + { + return false; + } + } + + + + void RestApiHierarchy::DeleteChildren(Children& children) + { + for (Children::iterator it = children.begin(); + it != children.end(); ++it) + { + delete it->second; + } + } + + + template <typename Handler> + void RestApiHierarchy::RegisterInternal(const RestApiPath& path, + Handler handler, + size_t level) + { + if (path.GetLevelCount() == level) + { + if (path.IsUniversalTrailing()) + { + universalHandlers_.Register(handler); + } + else + { + handlers_.Register(handler); + } + } + else + { + RestApiHierarchy* child; + if (path.IsWildcardLevel(level)) + { + child = &AddChild(wildcardChildren_, path.GetWildcardName(level)); + } + else + { + child = &AddChild(children_, path.GetLevelName(level)); + } + + child->RegisterInternal(path, handler, level + 1); + } + } + + + bool RestApiHierarchy::LookupResource(HttpHandler::Arguments& components, + const UriComponents& uri, + IVisitor& visitor, + size_t level) + { + if (uri.size() != 0 && + level > uri.size()) + { + return false; + } + + UriComponents trailing; + + // Look for an exact match on the resource of interest + if (uri.size() == 0 || + level == uri.size()) + { + if (!handlers_.IsEmpty() && + visitor.Visit(handlers_, uri, components, trailing)) + { + return true; + } + } + + + if (level < uri.size()) // A recursive call is possible + { + // Try and go down in the hierarchy, using an exact match for the child + Children::const_iterator child = children_.find(uri[level]); + if (child != children_.end()) + { + if (child->second->LookupResource(components, uri, visitor, level + 1)) + { + return true; + } + } + + // Try and go down in the hierarchy, using wildcard rules for children + for (child = wildcardChildren_.begin(); + child != wildcardChildren_.end(); ++child) + { + HttpHandler::Arguments subComponents = components; + subComponents[child->first] = uri[level]; + + if (child->second->LookupResource(subComponents, uri, visitor, level + 1)) + { + return true; + } + } + } + + + // As a last resort, call the universal handlers, if any + if (!universalHandlers_.IsEmpty()) + { + trailing.resize(uri.size() - level); + size_t pos = 0; + for (size_t i = level; i < uri.size(); i++, pos++) + { + trailing[pos] = uri[i]; + } + + assert(pos == trailing.size()); + + if (visitor.Visit(universalHandlers_, uri, components, trailing)) + { + return true; + } + } + + return false; + } + + + bool RestApiHierarchy::CanGenerateDirectory() const + { + return (universalHandlers_.IsEmpty() && + wildcardChildren_.empty()); + } + + + bool RestApiHierarchy::GetDirectory(Json::Value& result, + const UriComponents& uri, + size_t level) + { + if (uri.size() == level) + { + if (CanGenerateDirectory()) + { + result = Json::arrayValue; + + for (Children::const_iterator it = children_.begin(); + it != children_.end(); ++it) + { + result.append(it->first); + } + + return true; + } + else + { + return false; + } + } + + Children::const_iterator child = children_.find(uri[level]); + if (child != children_.end()) + { + if (child->second->GetDirectory(result, uri, level + 1)) + { + return true; + } + } + + for (child = wildcardChildren_.begin(); + child != wildcardChildren_.end(); ++child) + { + if (child->second->GetDirectory(result, uri, level + 1)) + { + return true; + } + } + + return false; + } + + + RestApiHierarchy::~RestApiHierarchy() + { + DeleteChildren(children_); + DeleteChildren(wildcardChildren_); + } + + void RestApiHierarchy::Register(const std::string& uri, + RestApiGetCall::Handler handler) + { + RestApiPath path(uri); + RegisterInternal(path, handler, 0); + } + + void RestApiHierarchy::Register(const std::string& uri, + RestApiPutCall::Handler handler) + { + RestApiPath path(uri); + RegisterInternal(path, handler, 0); + } + + void RestApiHierarchy::Register(const std::string& uri, + RestApiPostCall::Handler handler) + { + RestApiPath path(uri); + RegisterInternal(path, handler, 0); + } + + void RestApiHierarchy::Register(const std::string& uri, + RestApiDeleteCall::Handler handler) + { + RestApiPath path(uri); + RegisterInternal(path, handler, 0); + } + + void RestApiHierarchy::CreateSiteMap(Json::Value& target) const + { + target = Json::objectValue; + + /*std::string s = " "; + if (handlers_.HasHandler(HttpMethod_Get)) + { + s += "GET "; + } + + if (handlers_.HasHandler(HttpMethod_Post)) + { + s += "POST "; + } + + if (handlers_.HasHandler(HttpMethod_Put)) + { + s += "PUT "; + } + + if (handlers_.HasHandler(HttpMethod_Delete)) + { + s += "DELETE "; + } + + target = s;*/ + + for (Children::const_iterator it = children_.begin(); + it != children_.end(); ++it) + { + it->second->CreateSiteMap(target[it->first]); + } + + for (Children::const_iterator it = wildcardChildren_.begin(); + it != wildcardChildren_.end(); ++it) + { + it->second->CreateSiteMap(target["<" + it->first + ">"]); + } + } + + + bool RestApiHierarchy::LookupResource(const UriComponents& uri, + IVisitor& visitor) + { + HttpHandler::Arguments components; + return LookupResource(components, uri, visitor, 0); + } + + + + namespace + { + // Anonymous namespace to avoid clashes between compilation modules + + class AcceptedMethodsVisitor : public RestApiHierarchy::IVisitor + { + private: + std::set<HttpMethod>& methods_; + + public: + AcceptedMethodsVisitor(std::set<HttpMethod>& methods) : methods_(methods) + { + } + + virtual bool Visit(const RestApiHierarchy::Resource& resource, + const UriComponents& uri, + const HttpHandler::Arguments& components, + const UriComponents& trailing) + { + if (trailing.size() == 0) // Ignore universal handlers + { + if (resource.HasHandler(HttpMethod_Get)) + { + methods_.insert(HttpMethod_Get); + } + + if (resource.HasHandler(HttpMethod_Post)) + { + methods_.insert(HttpMethod_Post); + } + + if (resource.HasHandler(HttpMethod_Put)) + { + methods_.insert(HttpMethod_Put); + } + + if (resource.HasHandler(HttpMethod_Delete)) + { + methods_.insert(HttpMethod_Delete); + } + } + + return false; // Continue to check all the possible ways to access this URI + } + }; + } + + void RestApiHierarchy::GetAcceptedMethods(std::set<HttpMethod>& methods, + const UriComponents& uri) + { + HttpHandler::Arguments components; + AcceptedMethodsVisitor visitor(methods); + LookupResource(components, uri, visitor, 0); + + Json::Value d; + if (GetDirectory(d, uri)) + { + methods.insert(HttpMethod_Get); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/RestApi/RestApiHierarchy.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,164 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "RestApiGetCall.h" +#include "RestApiPostCall.h" +#include "RestApiPutCall.h" +#include "RestApiDeleteCall.h" + +#include <set> + +namespace Orthanc +{ + class RestApiHierarchy : public boost::noncopyable + { + public: + class Resource : public boost::noncopyable + { + private: + RestApiGetCall::Handler getHandler_; + RestApiPostCall::Handler postHandler_; + RestApiPutCall::Handler putHandler_; + RestApiDeleteCall::Handler deleteHandler_; + + public: + Resource(); + + bool HasHandler(HttpMethod method) const; + + void Register(RestApiGetCall::Handler handler) + { + getHandler_ = handler; + } + + void Register(RestApiPutCall::Handler handler) + { + putHandler_ = handler; + } + + void Register(RestApiPostCall::Handler handler) + { + postHandler_ = handler; + } + + void Register(RestApiDeleteCall::Handler handler) + { + deleteHandler_ = handler; + } + + bool IsEmpty() const; + + bool Handle(RestApiGetCall& call) const; + + bool Handle(RestApiPutCall& call) const; + + bool Handle(RestApiPostCall& call) const; + + bool Handle(RestApiDeleteCall& call) const; + }; + + + class IVisitor : public boost::noncopyable + { + public: + virtual ~IVisitor() + { + } + + virtual bool Visit(const Resource& resource, + const UriComponents& uri, + const HttpHandler::Arguments& components, + const UriComponents& trailing) = 0; + }; + + + private: + typedef std::map<std::string, RestApiHierarchy*> Children; + + Resource handlers_; + Children children_; + Children wildcardChildren_; + Resource universalHandlers_; + + static RestApiHierarchy& AddChild(Children& children, + const std::string& name); + + static void DeleteChildren(Children& children); + + template <typename Handler> + void RegisterInternal(const RestApiPath& path, + Handler handler, + size_t level); + + bool CanGenerateDirectory() const; + + bool LookupResource(HttpHandler::Arguments& components, + const UriComponents& uri, + IVisitor& visitor, + size_t level); + + bool GetDirectory(Json::Value& result, + const UriComponents& uri, + size_t level); + + public: + ~RestApiHierarchy(); + + void Register(const std::string& uri, + RestApiGetCall::Handler handler); + + void Register(const std::string& uri, + RestApiPutCall::Handler handler); + + void Register(const std::string& uri, + RestApiPostCall::Handler handler); + + void Register(const std::string& uri, + RestApiDeleteCall::Handler handler); + + void CreateSiteMap(Json::Value& target) const; + + bool GetDirectory(Json::Value& result, + const UriComponents& uri) + { + return GetDirectory(result, uri, 0); + } + + bool LookupResource(const UriComponents& uri, + IVisitor& visitor); + + void GetAcceptedMethods(std::set<HttpMethod>& methods, + const UriComponents& uri); + }; +}
--- a/Core/RestApi/RestApiOutput.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/RestApi/RestApiOutput.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -34,22 +34,28 @@ #include "RestApiOutput.h" #include <boost/lexical_cast.hpp> +#include <glog/logging.h> #include "../OrthancException.h" namespace Orthanc { RestApiOutput::RestApiOutput(HttpOutput& output) : - output_(output) + output_(output), + convertJsonToXml_(false) { alreadySent_ = false; } RestApiOutput::~RestApiOutput() { + } + + void RestApiOutput::Finalize() + { if (!alreadySent_) { - output_.SendHeader(HttpStatus_400_BadRequest); + output_.SendStatus(HttpStatus_404_NotFound); } } @@ -71,9 +77,26 @@ void RestApiOutput::AnswerJson(const Json::Value& value) { CheckStatus(); - Json::StyledWriter writer; - std::string s = writer.write(value); - output_.AnswerBufferWithContentType(s, "application/json", cookies_); + + if (convertJsonToXml_) + { +#if ORTHANC_PUGIXML_ENABLED == 1 + std::string s; + Toolbox::JsonToXml(s, value); + output_.SetContentType("application/xml"); + output_.SendBody(s); +#else + LOG(ERROR) << "Orthanc was compiled without XML support"; + throw OrthancException(ErrorCode_InternalError); +#endif + } + else + { + Json::StyledWriter writer; + output_.SetContentType("application/json"); + output_.SendBody(writer.write(value)); + } + alreadySent_ = true; } @@ -81,7 +104,8 @@ const std::string& contentType) { CheckStatus(); - output_.AnswerBufferWithContentType(buffer, contentType, cookies_); + output_.SetContentType(contentType.c_str()); + output_.SendBody(buffer); alreadySent_ = true; } @@ -90,7 +114,8 @@ const std::string& contentType) { CheckStatus(); - output_.AnswerBufferWithContentType(buffer, length, contentType, cookies_); + output_.SetContentType(contentType.c_str()); + output_.SendBody(buffer, length); alreadySent_ = true; } @@ -103,14 +128,16 @@ void RestApiOutput::SignalError(HttpStatus status) { - if (status != HttpStatus_403_Forbidden && + if (status != HttpStatus_400_BadRequest && + status != HttpStatus_403_Forbidden && + status != HttpStatus_500_InternalServerError && status != HttpStatus_415_UnsupportedMediaType) { throw OrthancException("This HTTP status is not allowed in a REST API"); } CheckStatus(); - output_.SendHeader(status); + output_.SendStatus(status); alreadySent_ = true; } @@ -135,7 +162,7 @@ v += ";max-age=" + boost::lexical_cast<std::string>(maxAge); } - cookies_[name] = v; + output_.SetCookie(name, v); } void RestApiOutput::ResetCookie(const std::string& name)
--- a/Core/RestApi/RestApiOutput.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/RestApi/RestApiOutput.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -44,7 +44,7 @@ private: HttpOutput& output_; bool alreadySent_; - HttpHandler::Arguments cookies_; + bool convertJsonToXml_; void CheckStatus(); @@ -63,6 +63,16 @@ alreadySent_ = true; } + void SetConvertJsonToXml(bool convert) + { + convertJsonToXml_ = convert; + } + + bool IsConvertJsonToXml() const + { + return convertJsonToXml_; + } + void AnswerFile(HttpFileSender& sender); void AnswerJson(const Json::Value& value); @@ -83,5 +93,7 @@ unsigned int maxAge = 0); void ResetCookie(const std::string& name); + + void Finalize(); }; }
--- a/Core/RestApi/RestApiPath.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/RestApi/RestApiPath.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -33,6 +33,8 @@ #include "../PrecompiledHeaders.h" #include "RestApiPath.h" +#include "../OrthancException.h" + #include <cassert> namespace Orthanc @@ -76,7 +78,7 @@ } } - bool RestApiPath::Match(Components& components, + bool RestApiPath::Match(HttpHandler::Arguments& components, UriComponents& trailing, const std::string& uriRaw) const { @@ -85,10 +87,12 @@ return Match(components, trailing, uri); } - bool RestApiPath::Match(Components& components, + bool RestApiPath::Match(HttpHandler::Arguments& components, UriComponents& trailing, const UriComponents& uri) const { + assert(uri_.size() == components_.size()); + if (uri.size() < uri_.size()) { return false; @@ -131,8 +135,46 @@ bool RestApiPath::Match(const UriComponents& uri) const { - Components components; + HttpHandler::Arguments components; UriComponents trailing; return Match(components, trailing, uri); } + + + bool RestApiPath::IsWildcardLevel(size_t level) const + { + assert(uri_.size() == components_.size()); + + if (level >= uri_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + return uri_[level].length() == 0; + } + + const std::string& RestApiPath::GetWildcardName(size_t level) const + { + assert(uri_.size() == components_.size()); + + if (!IsWildcardLevel(level)) + { + throw OrthancException(ErrorCode_BadParameterType); + } + + return components_[level]; + } + + const std::string& RestApiPath::GetLevelName(size_t level) const + { + assert(uri_.size() == components_.size()); + + if (IsWildcardLevel(level)) + { + throw OrthancException(ErrorCode_BadParameterType); + } + + return uri_[level]; + } } +
--- a/Core/RestApi/RestApiPath.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/RestApi/RestApiPath.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -33,6 +33,8 @@ #pragma once #include "../Toolbox.h" +#include "../HttpServer/HttpHandler.h" + #include <map> namespace Orthanc @@ -45,19 +47,34 @@ 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, + bool Match(HttpHandler::Arguments& components, UriComponents& trailing, const std::string& uriRaw) const; - bool Match(Components& components, + bool Match(HttpHandler::Arguments& components, UriComponents& trailing, const UriComponents& uri) const; bool Match(const UriComponents& uri) const; + + size_t GetLevelCount() const + { + return uri_.size(); + } + + bool IsWildcardLevel(size_t level) const; + + bool IsUniversalTrailing() const + { + return hasTrailing_; + } + + const std::string& GetWildcardName(size_t level) const; + + const std::string& GetLevelName(size_t level) const; + }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/RestApi/RestApiPostCall.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,69 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "RestApiCall.h" + +namespace Orthanc +{ + class RestApiPostCall : public RestApiCall + { + private: + const std::string& data_; + + public: + typedef void (*Handler) (RestApiPostCall& call); + + RestApiPostCall(RestApiOutput& output, + RestApi& context, + const HttpHandler::Arguments& httpHeaders, + const HttpHandler::Arguments& uriComponents, + const UriComponents& trailing, + const UriComponents& fullUri, + const std::string& data) : + RestApiCall(output, context, httpHeaders, uriComponents, trailing, fullUri), + data_(data) + { + } + + const std::string& GetPostBody() const + { + return data_; + } + + virtual bool ParseJsonRequest(Json::Value& result) const + { + return ParseJsonRequestInternal(result, GetPostBody().c_str()); + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/RestApi/RestApiPutCall.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,69 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "RestApiCall.h" + +namespace Orthanc +{ + class RestApiPutCall : public RestApiCall + { + private: + const std::string& data_; + + public: + typedef void (*Handler) (RestApiPutCall& call); + + RestApiPutCall(RestApiOutput& output, + RestApi& context, + const HttpHandler::Arguments& httpHeaders, + const HttpHandler::Arguments& uriComponents, + const UriComponents& trailing, + const UriComponents& fullUri, + const std::string& data) : + RestApiCall(output, context, httpHeaders, uriComponents, trailing, fullUri), + data_(data) + { + } + + const std::string& GetPutBody() const + { + return data_; + } + + virtual bool ParseJsonRequest(Json::Value& result) const + { + return ParseJsonRequestInternal(result, GetPutBody().c_str()); + } + }; +}
--- a/Core/SQLite/Connection.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/SQLite/Connection.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * + * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>, + * Medical Physics Department, CHU of Liege, Belgium * * Copyright (c) 2012 The Chromium Authors. All rights reserved. * @@ -34,15 +35,21 @@ **/ +#if ORTHANC_SQLITE_STANDALONE != 1 #include "../PrecompiledHeaders.h" +#endif + #include "Connection.h" +#include "OrthancSQLiteException.h" #include <memory> #include <cassert> #include <sqlite3.h> #include <string.h> +#if ORTHANC_SQLITE_STANDALONE != 1 #include <glog/logging.h> +#endif namespace Orthanc @@ -67,7 +74,7 @@ { if (!db_) { - throw OrthancException("SQLite: The database is not opened"); + throw OrthancSQLiteException("SQLite: The database is not opened"); } } @@ -75,7 +82,7 @@ { if (db_) { - throw OrthancException("SQLite: Connection is already open"); + throw OrthancSQLiteException("SQLite: Connection is already open"); } int err = sqlite3_open(path.c_str(), &db_); @@ -83,7 +90,7 @@ { Close(); db_ = NULL; - throw OrthancException("SQLite: Unable to open the database"); + throw OrthancSQLiteException("SQLite: Unable to open the database"); } // Execute PRAGMAs at this point @@ -129,7 +136,7 @@ { if (i->second->GetReferenceCount() >= 1) { - throw OrthancException("SQLite: This cached statement is already being referred to"); + throw OrthancSQLiteException("SQLite: This cached statement is already being referred to"); } return *i->second; @@ -145,13 +152,16 @@ bool Connection::Execute(const char* sql) { +#if ORTHANC_SQLITE_STANDALONE != 1 VLOG(1) << "SQLite::Connection::Execute " << sql; +#endif + CheckIsOpen(); int error = sqlite3_exec(db_, sql, NULL, NULL, NULL); if (error == SQLITE_ERROR) { - throw OrthancException("SQLite Execute error: " + std::string(sqlite3_errmsg(db_))); + throw OrthancSQLiteException("SQLite Execute error: " + std::string(sqlite3_errmsg(db_))); } else { @@ -272,7 +282,7 @@ { if (!transactionNesting_) { - throw OrthancException("Rolling back a nonexistent transaction"); + throw OrthancSQLiteException("Rolling back a nonexistent transaction"); } transactionNesting_--; @@ -291,7 +301,7 @@ { if (!transactionNesting_) { - throw OrthancException("Committing a nonexistent transaction"); + throw OrthancSQLiteException("Committing a nonexistent transaction"); } transactionNesting_--; @@ -359,7 +369,7 @@ if (err != SQLITE_OK) { delete func; - throw OrthancException("SQLite: Unable to register a function"); + throw OrthancSQLiteException("SQLite: Unable to register a function"); } return func; @@ -368,12 +378,15 @@ void Connection::FlushToDisk() { +#if ORTHANC_SQLITE_STANDALONE != 1 VLOG(1) << "SQLite::Connection::FlushToDisk"; +#endif + int err = sqlite3_wal_checkpoint(db_, NULL); if (err != SQLITE_OK) { - throw OrthancException("SQLite: Unable to flush the database"); + throw OrthancSQLiteException("SQLite: Unable to flush the database"); } } }
--- a/Core/SQLite/Connection.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/SQLite/Connection.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * + * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>, + * Medical Physics Department, CHU of Liege, Belgium * * Copyright (c) 2012 The Chromium Authors. All rights reserved. * @@ -40,7 +41,6 @@ #include "IScalarFunction.h" #include <string> -#include <boost/noncopyable.hpp> #include <map> struct sqlite3; @@ -52,7 +52,7 @@ { namespace SQLite { - class Connection : boost::noncopyable + class Connection : NonCopyable { friend class Statement; friend class Transaction;
--- a/Core/SQLite/FunctionContext.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/SQLite/FunctionContext.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * + * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>, + * Medical Physics Department, CHU of Liege, Belgium * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are @@ -31,8 +32,12 @@ **/ +#if ORTHANC_SQLITE_STANDALONE != 1 #include "../PrecompiledHeaders.h" +#endif + #include "FunctionContext.h" +#include "OrthancSQLiteException.h" #include <sqlite3.h> @@ -57,7 +62,7 @@ { if (index >= argc_) { - throw OrthancException(ErrorCode_ParameterOutOfRange); + throw OrthancSQLiteException("Parameter out of range"); } }
--- a/Core/SQLite/FunctionContext.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/SQLite/FunctionContext.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * + * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>, + * Medical Physics Department, CHU of Liege, Belgium * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are @@ -33,8 +34,6 @@ #pragma once -#include <boost/noncopyable.hpp> - #include "Statement.h" struct sqlite3_context; @@ -44,7 +43,7 @@ { namespace SQLite { - class FunctionContext : public boost::noncopyable + class FunctionContext : public NonCopyable { friend class Connection;
--- a/Core/SQLite/IScalarFunction.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/SQLite/IScalarFunction.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * + * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>, + * Medical Physics Department, CHU of Liege, Belgium * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are @@ -33,13 +34,14 @@ #pragma once +#include "NonCopyable.h" #include "FunctionContext.h" namespace Orthanc { namespace SQLite { - class IScalarFunction : public boost::noncopyable + class IScalarFunction : public NonCopyable { public: virtual ~IScalarFunction()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/SQLite/ITransaction.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,66 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * + * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>, + * Medical Physics Department, CHU of Liege, Belgium + * + * Copyright (c) 2012 The Chromium Authors. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc., the name of the CHU of Liege, + * nor the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **/ + + +#pragma once + +#include "NonCopyable.h" + +namespace Orthanc +{ + namespace SQLite + { + class ITransaction : public NonCopyable + { + public: + virtual ~ITransaction() + { + } + + // Begins the transaction. This uses the default sqlite "deferred" transaction + // type, which means that the DB lock is lazily acquired the next time the + // database is accessed, not in the begin transaction command. + virtual void Begin() = 0; + + // Rolls back the transaction. This will happen automatically if you do + // nothing when the transaction goes out of scope. + virtual void Rollback() = 0; + + // Commits the transaction, returning true on success. + virtual void Commit() = 0; + }; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/SQLite/NonCopyable.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,60 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * + * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>, + * Medical Physics Department, CHU of Liege, Belgium + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc., the name of the CHU of Liege, + * nor the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **/ + + +#pragma once + +namespace Orthanc +{ + namespace SQLite + { + // This class mimics "boost::noncopyable" + class NonCopyable + { + private: + NonCopyable(const NonCopyable&); + + NonCopyable& operator= (const NonCopyable&); + + protected: + NonCopyable() + { + } + + ~NonCopyable() + { + } + }; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/SQLite/OrthancSQLiteException.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,67 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * + * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>, + * Medical Physics Department, CHU of Liege, Belgium + * + * Copyright (c) 2012 The Chromium Authors. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc., the name of the CHU of Liege, + * nor the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **/ + + +#pragma once + + +#if ORTHANC_SQLITE_STANDALONE == 1 +#include <stdexcept> + +namespace Orthanc +{ + namespace SQLite + { + class OrthancSQLiteException : public ::std::runtime_error + { + public: + OrthancSQLiteException(const std::string& what) : + ::std::runtime_error(what) + { + } + + OrthancSQLiteException(const char* what) : + ::std::runtime_error(what) + { + } + }; + } +} + +#else +# include "../OrthancException.h" +# define OrthancSQLiteException ::Orthanc::OrthancException +#endif
--- a/Core/SQLite/README.txt Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/SQLite/README.txt Thu May 21 16:58:30 2015 +0200 @@ -19,6 +19,17 @@ coding conventions. +Reuse in another software +========================= + +To use the Orthanc SQLite wrapper in another project than Orthanc, you +just have to define the "ORTHANC_SQLITE_STANDALONE" macro. + +All the C++ exceptions generated by the wrapper will be objects of the +class "::Orthanc::SQLite::OrthancSQLiteException", that derives from +the standard exception class "::std::runtime_error". + + Licensing ========= @@ -26,4 +37,4 @@ order to respect the original license of the code. It is pretty straightforward to extract the code from this folder and -to include it in another project. +to include it in another project.
--- a/Core/SQLite/Statement.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/SQLite/Statement.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * + * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>, + * Medical Physics Department, CHU of Liege, Belgium * * Copyright (c) 2012 The Chromium Authors. All rights reserved. * @@ -34,16 +35,25 @@ **/ +#if ORTHANC_SQLITE_STANDALONE != 1 #include "../PrecompiledHeaders.h" +#endif + #include "Statement.h" - #include "Connection.h" -#include "../Toolbox.h" -#include <boost/lexical_cast.hpp> #include <sqlite3.h> #include <string.h> +#include <stdio.h> +#include <algorithm> + +#if ORTHANC_SQLITE_STANDALONE != 1 #include <glog/logging.h> +#endif + +#if defined(_MSC_VER) +#define snprintf _snprintf +#endif namespace Orthanc { @@ -54,7 +64,9 @@ bool succeeded = (err == SQLITE_OK || err == SQLITE_ROW || err == SQLITE_DONE); if (!succeeded) { - throw OrthancException("SQLite error code " + boost::lexical_cast<std::string>(err)); + char buffer[128]; + snprintf(buffer, sizeof(buffer) - 1, "SQLite error code %d", err); + throw OrthancSQLiteException(buffer); } return err; @@ -65,11 +77,13 @@ if (err == SQLITE_RANGE) { // Binding to a non-existent variable is evidence of a serious error. - throw OrthancException("Bind value out of range"); + throw OrthancSQLiteException("Bind value out of range"); } else if (err != SQLITE_OK) { - throw OrthancException("SQLite error code " + boost::lexical_cast<std::string>(err)); + char buffer[128]; + snprintf(buffer, sizeof(buffer) - 1, "SQLite error code %d", err); + throw OrthancSQLiteException(buffer); } } @@ -108,13 +122,19 @@ bool Statement::Run() { +#if ORTHANC_SQLITE_STANDALONE != 1 VLOG(1) << "SQLite::Statement::Run " << sqlite3_sql(GetStatement()); +#endif + return CheckError(sqlite3_step(GetStatement())) == SQLITE_DONE; } bool Statement::Step() { +#if ORTHANC_SQLITE_STANDALONE != 1 VLOG(1) << "SQLite::Statement::Step " << sqlite3_sql(GetStatement()); +#endif + return CheckError(sqlite3_step(GetStatement())) == SQLITE_ROW; } @@ -206,7 +226,7 @@ ColumnType Statement::GetDeclaredColumnType(int col) const { std::string column_type(sqlite3_column_decltype(GetStatement(), col)); - Toolbox::ToLowerCase(column_type); + std::transform(column_type.begin(), column_type.end(), column_type.begin(), tolower); if (column_type == "integer") return COLUMN_TYPE_INTEGER;
--- a/Core/SQLite/Statement.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/SQLite/Statement.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * + * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>, + * Medical Physics Department, CHU of Liege, Belgium * * Copyright (c) 2012 The Chromium Authors. All rights reserved. * @@ -36,13 +37,13 @@ #pragma once -#include "../OrthancException.h" +#include "NonCopyable.h" +#include "OrthancSQLiteException.h" #include "StatementId.h" #include "StatementReference.h" #include <vector> #include <stdint.h> -#include <boost/noncopyable.hpp> #if ORTHANC_BUILD_UNIT_TESTS == 1 #include <gtest/gtest_prod.h> @@ -68,7 +69,7 @@ COLUMN_TYPE_NULL = 5 }; - class Statement : public boost::noncopyable + class Statement : public NonCopyable { friend class Connection;
--- a/Core/SQLite/StatementId.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/SQLite/StatementId.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * + * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>, + * Medical Physics Department, CHU of Liege, Belgium * * Copyright (c) 2012 The Chromium Authors. All rights reserved. * @@ -34,7 +35,10 @@ **/ +#if ORTHANC_SQLITE_STANDALONE != 1 #include "../PrecompiledHeaders.h" +#endif + #include "StatementId.h" #include <string.h>
--- a/Core/SQLite/StatementId.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/SQLite/StatementId.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * + * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>, + * Medical Physics Department, CHU of Liege, Belgium * * Copyright (c) 2012 The Chromium Authors. All rights reserved. *
--- a/Core/SQLite/StatementReference.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/SQLite/StatementReference.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * + * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>, + * Medical Physics Department, CHU of Liege, Belgium * * Copyright (c) 2012 The Chromium Authors. All rights reserved. * @@ -34,13 +35,18 @@ **/ +#if ORTHANC_SQLITE_STANDALONE != 1 #include "../PrecompiledHeaders.h" +#endif + #include "StatementReference.h" +#include "OrthancSQLiteException.h" -#include "../OrthancException.h" +#if ORTHANC_SQLITE_STANDALONE != 1 +#include <glog/logging.h> +#endif #include <cassert> -#include <glog/logging.h> #include "sqlite3.h" namespace Orthanc @@ -65,7 +71,7 @@ { if (database == NULL || sql == NULL) { - throw OrthancException(ErrorCode_ParameterOutOfRange); + throw OrthancSQLiteException("Parameter out of range"); } root_ = NULL; @@ -74,7 +80,7 @@ int error = sqlite3_prepare_v2(database, sql, -1, &statement_, NULL); if (error != SQLITE_OK) { - throw OrthancException("SQLite: " + std::string(sqlite3_errmsg(database))); + throw OrthancSQLiteException("SQLite: " + std::string(sqlite3_errmsg(database))); } assert(IsRoot()); @@ -109,7 +115,9 @@ // an exception because: // http://www.parashift.com/c++-faq/dtors-shouldnt-throw.html +#if ORTHANC_SQLITE_STANDALONE != 1 LOG(ERROR) << "Bad value of the reference counter"; +#endif } else if (statement_ != NULL) { @@ -124,7 +132,9 @@ // an exception because: // http://www.parashift.com/c++-faq/dtors-shouldnt-throw.html +#if ORTHANC_SQLITE_STANDALONE != 1 LOG(ERROR) << "Bad value of the reference counter"; +#endif } else {
--- a/Core/SQLite/StatementReference.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/SQLite/StatementReference.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * + * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>, + * Medical Physics Department, CHU of Liege, Belgium * * Copyright (c) 2012 The Chromium Authors. All rights reserved. * @@ -36,7 +37,8 @@ #pragma once -#include <boost/noncopyable.hpp> +#include "NonCopyable.h" + #include <stdint.h> #include <cassert> #include <stdlib.h> @@ -48,7 +50,7 @@ { namespace SQLite { - class StatementReference : boost::noncopyable + class StatementReference : NonCopyable { private: StatementReference* root_; // Only used for non-root nodes
--- a/Core/SQLite/Transaction.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/SQLite/Transaction.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * + * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>, + * Medical Physics Department, CHU of Liege, Belgium * * Copyright (c) 2012 The Chromium Authors. All rights reserved. * @@ -34,8 +35,12 @@ **/ +#if ORTHANC_SQLITE_STANDALONE != 1 #include "../PrecompiledHeaders.h" +#endif + #include "Transaction.h" +#include "OrthancSQLiteException.h" namespace Orthanc { @@ -59,13 +64,13 @@ { if (isOpen_) { - throw OrthancException("SQLite: Beginning a transaction twice!"); + throw OrthancSQLiteException("SQLite: Beginning a transaction twice!"); } isOpen_ = connection_.BeginTransaction(); if (!isOpen_) { - throw OrthancException("SQLite: Unable to create a transaction"); + throw OrthancSQLiteException("SQLite: Unable to create a transaction"); } } @@ -73,8 +78,8 @@ { if (!isOpen_) { - throw OrthancException("SQLite: Attempting to roll back a nonexistent transaction. " - "Did you remember to call Begin()?"); + throw OrthancSQLiteException("SQLite: Attempting to roll back a nonexistent transaction. " + "Did you remember to call Begin()?"); } isOpen_ = false; @@ -86,15 +91,15 @@ { if (!isOpen_) { - throw OrthancException("SQLite: Attempting to roll back a nonexistent transaction. " - "Did you remember to call Begin()?"); + throw OrthancSQLiteException("SQLite: Attempting to roll back a nonexistent transaction. " + "Did you remember to call Begin()?"); } isOpen_ = false; if (!connection_.CommitTransaction()) { - throw OrthancException("SQLite: Failure when committing the transaction"); + throw OrthancSQLiteException("SQLite: Failure when committing the transaction"); } } }
--- a/Core/SQLite/Transaction.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/SQLite/Transaction.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * + * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>, + * Medical Physics Department, CHU of Liege, Belgium * * Copyright (c) 2012 The Chromium Authors. All rights reserved. * @@ -37,12 +38,13 @@ #pragma once #include "Connection.h" +#include "ITransaction.h" namespace Orthanc { namespace SQLite { - class Transaction : public boost::noncopyable + class Transaction : public ITransaction { private: Connection& connection_; @@ -53,22 +55,17 @@ public: explicit Transaction(Connection& connection); - ~Transaction(); + + virtual ~Transaction(); // Returns true when there is a transaction that has been successfully begun. bool IsOpen() const { return isOpen_; } - // Begins the transaction. This uses the default sqlite "deferred" transaction - // type, which means that the DB lock is lazily acquired the next time the - // database is accessed, not in the begin transaction command. - void Begin(); + virtual void Begin(); - // Rolls back the transaction. This will happen automatically if you do - // nothing when the transaction goes out of scope. - void Rollback(); + virtual void Rollback(); - // Commits the transaction, returning true on success. - void Commit(); + virtual void Commit(); }; } }
--- a/Core/Toolbox.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/Toolbox.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -44,9 +44,14 @@ #include <algorithm> #include <ctype.h> #include <boost/regex.hpp> +#include <glog/logging.h> #if defined(_WIN32) #include <windows.h> +#include <process.h> // For "_spawnvp()" +#else +#include <unistd.h> // For "execvp()" +#include <sys/wait.h> // For "waitpid()" #endif #if defined(__APPLE__) && defined(__MACH__) @@ -54,7 +59,7 @@ #include <limits.h> /* PATH_MAX */ #endif -#if defined(__linux) || defined(__FreeBSD_kernel__) +#if defined(__linux) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) #include <limits.h> /* PATH_MAX */ #include <signal.h> #include <unistd.h> @@ -70,8 +75,8 @@ #include "../Resources/ThirdParty/base64/base64.h" -#ifdef _MSC_VER -// Patch for the missing "_strtoll" symbol when compiling with Visual Studio +#if defined(_MSC_VER) && (_MSC_VER < 1800) +// Patch for the missing "_strtoll" symbol when compiling with Visual Studio < 2013 extern "C" { int64_t _strtoi64(const char *nptr, char **endptr, int base); @@ -83,6 +88,12 @@ #endif +#if ORTHANC_PUGIXML_ENABLED == 1 +#include "ChunkedBuffer.h" +#include <pugixml.hpp> +#endif + + namespace Orthanc { static bool finish; @@ -105,7 +116,7 @@ { #if defined(_WIN32) ::Sleep(static_cast<DWORD>(microSeconds / static_cast<uint64_t>(1000))); -#elif defined(__linux) || defined(__APPLE__) || defined(__FreeBSD_kernel__) +#elif defined(__linux) || defined(__APPLE__) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) usleep(microSeconds); #else #error Support your platform here @@ -113,7 +124,7 @@ } - void Toolbox::ServerBarrier() + static void ServerBarrierInternal(const bool* stopFlag) { #if defined(_WIN32) SetConsoleCtrlHandler(ConsoleControlHandler, true); @@ -123,10 +134,11 @@ signal(SIGTERM, SignalHandler); #endif + // Active loop that awakens every 100ms finish = false; - while (!finish) + while (!(*stopFlag || finish)) { - USleep(100000); + Toolbox::USleep(100 * 1000); } #if defined(_WIN32) @@ -139,6 +151,17 @@ } + void Toolbox::ServerBarrier(const bool& stopFlag) + { + ServerBarrierInternal(&stopFlag); + } + + void Toolbox::ServerBarrier() + { + const bool stopFlag = false; + ServerBarrierInternal(&stopFlag); + } + void Toolbox::ToUpperCase(std::string& s) { @@ -284,6 +307,28 @@ } + void Toolbox::TruncateUri(UriComponents& target, + const UriComponents& source, + size_t fromLevel) + { + target.clear(); + + if (source.size() > fromLevel) + { + target.resize(source.size() - fromLevel); + + size_t j = 0; + for (size_t i = fromLevel; i < source.size(); i++, j++) + { + target[j] = source[i]; + } + + assert(j == target.size()); + } + } + + + bool Toolbox::IsChildUri(const UriComponents& baseUri, const UriComponents& testedUri) { @@ -449,7 +494,7 @@ #if defined(_WIN32) - std::string Toolbox::GetPathToExecutable() + static std::string GetPathToExecutableInternal() { // Yes, this is ugly, but there is no simple way to get the // required buffer size, so we use a big constant @@ -458,8 +503,8 @@ return std::string(&buffer[0]); } -#elif defined(__linux) || defined(__FreeBSD_kernel__) - std::string Toolbox::GetPathToExecutable() +#elif defined(__linux) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) + static std::string GetPathToExecutableInternal() { std::vector<char> buffer(PATH_MAX + 1); ssize_t bytes = readlink("/proc/self/exe", &buffer[0], buffer.size() - 1); @@ -472,7 +517,7 @@ } #elif defined(__APPLE__) && defined(__MACH__) - std::string Toolbox::GetPathToExecutable() + static std::string GetPathToExecutableInternal() { char pathbuf[PATH_MAX + 1]; unsigned int bufsize = static_cast<int>(sizeof(pathbuf)); @@ -487,10 +532,17 @@ #endif + std::string Toolbox::GetPathToExecutable() + { + boost::filesystem::path p(GetPathToExecutableInternal()); + return boost::filesystem::absolute(p).string(); + } + + std::string Toolbox::GetDirectoryOfExecutable() { - boost::filesystem::path p(GetPathToExecutable()); - return p.parent_path().string(); + boost::filesystem::path p(GetPathToExecutableInternal()); + return boost::filesystem::absolute(p.parent_path()).string(); } @@ -499,18 +551,71 @@ { const char* encoding; + + // http://bradleyross.users.sourceforge.net/docs/dicom/doc/src-html/org/dcm4che2/data/SpecificCharacterSet.html switch (sourceEncoding) { case Encoding_Utf8: // Already in UTF-8: No conversion is required return source; + case Encoding_Ascii: + return ConvertToAscii(source); + case Encoding_Latin1: encoding = "ISO-8859-1"; break; + case Encoding_Latin2: + encoding = "ISO-8859-2"; + break; + + case Encoding_Latin3: + encoding = "ISO-8859-3"; + break; + + case Encoding_Latin4: + encoding = "ISO-8859-4"; + break; + + case Encoding_Latin5: + encoding = "ISO-8859-9"; + break; + + case Encoding_Cyrillic: + encoding = "ISO-8859-5"; + break; + + case Encoding_Windows1251: + encoding = "WINDOWS-1251"; + break; + + case Encoding_Arabic: + encoding = "ISO-8859-6"; + break; + + case Encoding_Greek: + encoding = "ISO-8859-7"; + break; + + case Encoding_Hebrew: + encoding = "ISO-8859-8"; + break; + + case Encoding_Japanese: + encoding = "SHIFT-JIS"; + break; + + case Encoding_Chinese: + encoding = "GB18030"; + break; + + case Encoding_Thai: + encoding = "TIS620.2533-0"; + break; + default: - throw OrthancException(ErrorCode_ParameterOutOfRange); + throw OrthancException(ErrorCode_NotImplemented); } try @@ -532,7 +637,7 @@ result.reserve(source.size() + 1); for (size_t i = 0; i < source.size(); i++) { - if (source[i] < 128 && source[i] >= 0 && !iscntrl(source[i])) + if (source[i] <= 127 && source[i] >= 0 && !iscntrl(source[i])) { result.push_back(source[i]); } @@ -792,5 +897,220 @@ } } } + + + bool Toolbox::IsExistingFile(const std::string& path) + { + return boost::filesystem::exists(path); + } + + +#if ORTHANC_PUGIXML_ENABLED == 1 + class ChunkedBufferWriter : public pugi::xml_writer + { + private: + ChunkedBuffer buffer_; + + public: + virtual void write(const void *data, size_t size) + { + if (size > 0) + { + buffer_.AddChunk(reinterpret_cast<const char*>(data), size); + } + } + + void Flatten(std::string& s) + { + buffer_.Flatten(s); + } + }; + + + static void JsonToXmlInternal(pugi::xml_node& target, + const Json::Value& source, + const std::string& arrayElement) + { + // http://jsoncpp.sourceforge.net/value_8h_source.html#l00030 + + switch (source.type()) + { + case Json::nullValue: + { + target.append_child(pugi::node_pcdata).set_value("null"); + break; + } + + case Json::intValue: + { + std::string s = boost::lexical_cast<std::string>(source.asInt()); + target.append_child(pugi::node_pcdata).set_value(s.c_str()); + break; + } + + case Json::uintValue: + { + std::string s = boost::lexical_cast<std::string>(source.asUInt()); + target.append_child(pugi::node_pcdata).set_value(s.c_str()); + break; + } + + case Json::realValue: + { + std::string s = boost::lexical_cast<std::string>(source.asFloat()); + target.append_child(pugi::node_pcdata).set_value(s.c_str()); + break; + } + + case Json::stringValue: + { + target.append_child(pugi::node_pcdata).set_value(source.asString().c_str()); + break; + } + + case Json::booleanValue: + { + target.append_child(pugi::node_pcdata).set_value(source.asBool() ? "true" : "false"); + break; + } + + case Json::arrayValue: + { + for (Json::Value::ArrayIndex i = 0; i < source.size(); i++) + { + pugi::xml_node node = target.append_child(); + node.set_name(arrayElement.c_str()); + JsonToXmlInternal(node, source[i], arrayElement); + } + break; + } + + case Json::objectValue: + { + Json::Value::Members members = source.getMemberNames(); + + for (size_t i = 0; i < members.size(); i++) + { + pugi::xml_node node = target.append_child(); + node.set_name(members[i].c_str()); + JsonToXmlInternal(node, source[members[i]], arrayElement); + } + + break; + } + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + void Toolbox::JsonToXml(std::string& target, + const Json::Value& source, + const std::string& rootElement, + const std::string& arrayElement) + { + pugi::xml_document doc; + + pugi::xml_node n = doc.append_child(rootElement.c_str()); + JsonToXmlInternal(n, source, arrayElement); + + pugi::xml_node decl = doc.prepend_child(pugi::node_declaration); + decl.append_attribute("version").set_value("1.0"); + decl.append_attribute("encoding").set_value("utf-8"); + + ChunkedBufferWriter writer; + doc.save(writer, " ", pugi::format_default, pugi::encoding_utf8); + writer.Flatten(target); + } + +#endif + + + void Toolbox::ExecuteSystemCommand(const std::string& command, + const std::vector<std::string>& arguments) + { + // Convert the arguments as a C array + std::vector<char*> args(arguments.size() + 2); + + args.front() = const_cast<char*>(command.c_str()); + + for (size_t i = 0; i < arguments.size(); i++) + { + args[i + 1] = const_cast<char*>(arguments[i].c_str()); + } + + args.back() = NULL; + + int status; + +#if defined(_WIN32) + // http://msdn.microsoft.com/en-us/library/275khfab.aspx + status = static_cast<int>(_spawnvp(_P_OVERLAY, command.c_str(), &args[0])); + +#else + int pid = fork(); + + if (pid == -1) + { + // Error in fork() + LOG(ERROR) << "Cannot fork a child process"; + throw OrthancException(ErrorCode_SystemCommand); + } + else if (pid == 0) + { + // Execute the system command in the child process + execvp(command.c_str(), &args[0]); + + // We should never get here + _exit(1); + } + else + { + // Wait for the system command to exit + waitpid(pid, &status, 0); + } +#endif + + if (status != 0) + { + LOG(ERROR) << "System command failed with status code " << status; + throw OrthancException(ErrorCode_SystemCommand); + } + } + + + bool Toolbox::IsInteger(const std::string& str) + { + std::string s = StripSpaces(str); + + if (s.size() == 0) + { + return false; + } + + size_t pos = 0; + if (s[0] == '-') + { + if (s.size() == 1) + { + return false; + } + + pos = 1; + } + + while (pos < s.size()) + { + if (!isdigit(s[pos])) + { + return false; + } + + pos++; + } + + return true; + } }
--- a/Core/Toolbox.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/Toolbox.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -38,6 +38,10 @@ #include <vector> #include <string> +#if ORTHANC_PUGIXML_ENABLED == 1 +#include <json/json.h> +#endif + namespace Orthanc { typedef std::vector<std::string> UriComponents; @@ -48,6 +52,8 @@ namespace Toolbox { + void ServerBarrier(const bool& stopFlag); + void ServerBarrier(); void ToUpperCase(std::string& s); // Inplace version @@ -73,6 +79,10 @@ void SplitUriComponents(UriComponents& components, const std::string& uri); + void TruncateUri(UriComponents& target, + const UriComponents& source, + size_t fromLevel); + bool IsChildUri(const UriComponents& baseUri, const UriComponents& testedUri); @@ -130,5 +140,19 @@ const std::string& source); void CreateDirectory(const std::string& path); + + bool IsExistingFile(const std::string& path); + +#if ORTHANC_PUGIXML_ENABLED == 1 + void JsonToXml(std::string& target, + const Json::Value& source, + const std::string& rootElement = "root", + const std::string& arrayElement = "item"); +#endif + + void ExecuteSystemCommand(const std::string& command, + const std::vector<std::string>& arguments); + + bool IsInteger(const std::string& str); } }
--- a/Core/Uuid.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/Uuid.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Core/Uuid.h Wed Jun 25 15:34:40 2014 +0200 +++ b/Core/Uuid.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -43,6 +43,8 @@ * http://stackoverflow.com/questions/246930/is-there-any-difference-between-a-guid-and-a-uuid **/ +#include "Toolbox.h" + namespace Orthanc { namespace Toolbox @@ -69,6 +71,16 @@ { return path_; } + + void Write(const std::string& content) + { + Toolbox::WriteFile(content, path_); + } + + void Read(std::string& content) const + { + Toolbox::ReadFile(content, path_); + } }; } }
--- a/DarwinCompilation.txt Wed Jun 25 15:34:40 2014 +0200 +++ b/DarwinCompilation.txt Thu May 21 16:58:30 2015 +0200 @@ -28,7 +28,7 @@ ---------------------------- # cd ~/OrthancBuild -# cmake -GXcode -DCMAKE_OSX_DEPLOYMENT_TARGET=10.8 -DSTATIC_BUILD=ON -DSTANDALONE_BUILD=ON .. +# cmake -GXcode -DCMAKE_OSX_DEPLOYMENT_TARGET=10.8 -DSTATIC_BUILD=ON -DSTANDALONE_BUILD=ON -DALLOW_DOWNLOADS=ON ~/Orthanc NB: Adapt the value of "CMAKE_OSX_DEPLOYMENT_TARGET" with respect to your version of XCode.
--- a/INSTALL Wed Jun 25 15:34:40 2014 +0200 +++ b/INSTALL Thu May 21 16:58:30 2015 +0200 @@ -54,16 +54,18 @@ -Native Windows build with Microsoft Visual Studio 2005 ------------------------------------------------------- +Native Windows build with Microsoft Visual Studio +------------------------------------------------- # cd [...]\OrthancBuild -# cmake -DSTANDALONE_BUILD=ON -G "Visual Studio 8 2005" [...]\Orthanc +# cmake -DSTANDALONE_BUILD=ON -DSTATIC_BUILD=ON -DALLOW_DOWNLOADS=ON -G "Visual Studio 8 2005" [...]\Orthanc Then open the "[...]/OrthancBuild/Orthanc.sln" with Visual Studio. NOTES: -* More recent versions of Visual Studio should also work. +* More recent versions of Visual Studio than 2005 should also + work. Type "cmake" without arguments to have the list of generators + that are available on your computer. * You will have to install the Platform SDK (version 6 or above) for Visual Studio 2005: http://en.wikipedia.org/wiki/Microsoft_Windows_SDK.
--- a/LinuxCompilation.txt Wed Jun 25 15:34:40 2014 +0200 +++ b/LinuxCompilation.txt Thu May 21 16:58:30 2015 +0200 @@ -9,15 +9,17 @@ statically linking against all the third-party dependencies. In this case, the system-wide libraries will not be used. The build tool (CMake) will download the sources of all the required packages and -automatically compile them. This process should work on all the Linux -distributions. +automatically compile them. -We make the assumption that Orthanc source code is placed in the -folder "~/Orthanc" and that the binaries will be compiled to -"~/OrthancBuild". +This process should work on any Linux distribution, provided that a +C/C++ compiler ("build-essential" in Debian-based systems), the Python +interpreter, CMake, the "unzip" system tool, and the development +package for libuuid ("uuid-dev" in Debian) are installed. -To build binaries with debug information: +We now make the assumption that Orthanc source code is placed in the +folder "~/Orthanc" and that the binaries will be compiled to +"~/OrthancBuild". To build binaries with debug information: # cd ~/OrthancBuild # cmake -DSTATIC_BUILD=ON -DCMAKE_BUILD_TYPE=Debug ~/Orthanc @@ -33,10 +35,17 @@ # make doc -Note: When the "STATIC_BUILD" option is set to "ON", the build tool +Note 1- When the "STATIC_BUILD" option is set to "ON", the build tool will not ask you the permission to download packages from the Internet. +Note 2- If the development package of libuuid was not installed when +first invoking cmake, you will have to manually remove the build +directory ("rm -rf ~/OrthancBuild") after installing this package, +then run cmake again. + +Note 3- To build the documentation, you will have to install doxyen. + Use system-wide libraries under Linux ===================================== @@ -51,6 +60,7 @@ # cmake -DCMAKE_BUILD_TYPE=Debug ~/Orthanc # make +Note that to build the documentation, you will have to install doxyen. However, on some Linux distributions, it is still required to download and static link against some third-party dependencies, e.g. when the @@ -77,6 +87,7 @@ -DUSE_SYSTEM_DCMTK=OFF \ -DUSE_SYSTEM_MONGOOSE=OFF \ -DUSE_SYSTEM_JSONCPP=OFF \ + -DUSE_SYSTEM_PUGIXML=OFF \ -DENABLE_JPEG=OFF \ -DENABLE_JPEG_LOSSLESS=OFF \ ~/Orthanc @@ -95,6 +106,7 @@ -DUSE_SYSTEM_GOOGLE_LOG=OFF \ -DUSE_SYSTEM_MONGOOSE=OFF \ -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON \ + -DUSE_SYSTEM_PUGIXML=OFF \ -DENABLE_JPEG=OFF \ -DENABLE_JPEG_LOSSLESS=OFF \ ~/Orthanc @@ -107,35 +119,34 @@ uuid-dev libcurl4-openssl-dev liblua5.1-0-dev \ libgoogle-glog-dev libgtest-dev libpng-dev \ libsqlite3-dev libssl-dev zlib1g-dev libdcmtk2-dev \ - libboost-all-dev libwrap0-dev libjsoncpp-dev + libboost-all-dev libwrap0-dev libjsoncpp-dev libpugixml-dev # cmake -DALLOW_DOWNLOADS=ON \ -DUSE_SYSTEM_MONGOOSE=OFF \ -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON \ - -DENABLE_JPEG=OFF \ - -DENABLE_JPEG_LOSSLESS=OFF \ + -DDCMTK_LIBRARIES=dcmjpls \ ~/Orthanc Note: Have also a look at the official package: http://anonscm.debian.org/viewvc/debian-med/trunk/packages/orthanc/trunk/debian/ -SUPPORTED - Ubuntu 12.04 LTS ----------------------------- +SUPPORTED - Ubuntu 12.04.5 LTS +------------------------------ # sudo apt-get install build-essential unzip cmake mercurial \ uuid-dev libcurl4-openssl-dev liblua5.1-0-dev \ libgtest-dev libpng-dev libsqlite3-dev libssl-dev \ - zlib1g-dev libdcmtk2-dev libboost-all-dev libwrap0-dev + zlib1g-dev libdcmtk2-dev libboost1.48-all-dev libwrap0-dev \ + libcharls-dev -# cmake "-DDCMTK_LIBRARIES=wrap;oflog" \ +# cmake "-DDCMTK_LIBRARIES=boost_locale;CharLS;dcmjpls;wrap;oflog" \ -DALLOW_DOWNLOADS=ON \ -DUSE_SYSTEM_MONGOOSE=OFF \ -DUSE_SYSTEM_JSONCPP=OFF \ -DUSE_SYSTEM_GOOGLE_LOG=OFF \ + -DUSE_SYSTEM_PUGIXML=OFF \ -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON \ - -DENABLE_JPEG=OFF \ - -DENABLE_JPEG_LOSSLESS=OFF \ ~/Orthanc @@ -155,6 +166,7 @@ -DUSE_SYSTEM_MONGOOSE=OFF \ -DUSE_SYSTEM_JSONCPP=OFF \ -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON \ + -DUSE_SYSTEM_PUGIXML=OFF \ ~/Orthanc @@ -165,6 +177,7 @@ -DUSE_SYSTEM_MONGOOSE=OFF \ -DUSE_SYSTEM_JSONCPP=OFF \ -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON \ + -DUSE_SYSTEM_PUGIXML=OFF \ -DENABLE_JPEG=OFF \ -DENABLE_JPEG_LOSSLESS=OFF \ ~/Orthanc @@ -183,29 +196,52 @@ -DALLOW_DOWNLOADS=ON \ -DUSE_SYSTEM_MONGOOSE=OFF \ -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON \ + -DUSE_SYSTEM_PUGIXML=OFF \ -DENABLE_JPEG=OFF \ -DENABLE_JPEG_LOSSLESS=OFF \ ~/Orthanc -SUPPORTED - Fedora 18/19/20 ---------------------------- +SUPPORTED - Fedora 19/20 +------------------------ # sudo yum install make automake gcc gcc-c++ python cmake \ boost-devel curl-devel dcmtk-devel glog-devel \ gtest-devel libpng-devel libsqlite3x-devel libuuid-devel \ - mongoose-devel openssl-devel jsoncpp-devel lua-devel + mongoose-devel openssl-devel jsoncpp-devel lua-devel pugixml-devel -# cmake -DENABLE_JPEG=OFF \ - -DENABLE_JPEG_LOSSLESS=OFF \ - ~/Orthanc +# cmake "-DDCMTK_LIBRARIES=CharLS" \ + -DSYSTEM_MONGOOSE_USE_CALLBACKS=OFF \ + ~/Orthanc Note: Have also a look at the official package: http://pkgs.fedoraproject.org/cgit/orthanc.git/tree/?h=f18 +SUPPORTED - Ubuntu 14.04 LTS +---------------------------- + +# cmake -DALLOW_DOWNLOADS=ON \ + -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON \ + -DUSE_SYSTEM_MONGOOSE=OFF \ + -DDCMTK_LIBRARIES=dcmjpls \ + ~/Orthanc + + + +SUPPORTED - FreeBSD 10.1 +------------------------ + +# pkg install jsoncpp pugixml lua51 curl googletest dcmtk cmake \ + e2fsprogs-libuuid glog boost-libs sqlite3 python libiconv + +# cmake -DALLOW_DOWNLOADS=ON \ + -DUSE_SYSTEM_MONGOOSE=OFF \ + -DDCMTK_LIBRARIES="dcmdsig;charls;dcmjpls" \ + ~/Orthanc + Other Linux distributions?
--- a/NEWS Wed Jun 25 15:34:40 2014 +0200 +++ b/NEWS Thu May 21 16:58:30 2015 +0200 @@ -1,9 +1,193 @@ Pending changes in the mainline =============================== -* Official support of OS X (Darwin) +Major +----- + +* "?expand" flag for URIs "/patients", "/studies" and "/series" +* "/tools/find" URI to search for DICOM resources from REST +* Support of FreeBSD + +Minor +----- + +* Speed-up in Orthanc Explorer for large amount of images +* Speed-up of the C-Find SCP server of Orthanc +* Allow replacing PatientID/StudyInstanceUID/SeriesInstanceUID from Lua scripts + +Fixes +----- + +* Prevent freeze on C-FIND if no DICOM tag is to be returned +* Fix slow C-Store SCP on recent versions of Linux, if + USE_SYSTEM_DCMTK is set to OFF (http://forum.dcmtk.org/viewtopic.php?f=1&t=4009) +* Fix issue 30 (QR response missing "Query/Retrieve Level" (008,0052)) +* Fix issue 32 (Cyrillic symbols): Introduction of the "Windows1251" encoding +* Plugins now receive duplicated GET arguments in their REST callbacks + + +Version 0.8.6 (2015/02/12) +========================== + +Major +----- + +* URIs to get all the parents of a given resource in a single REST call +* Instances without PatientID are now allowed +* Support of HTTP proxy to access Orthanc peers + +Minor +----- + +* Support of Tudor DICOM in Query/Retrieve +* More flexible "/modify" and "/anonymize" for single instance +* Access to called AET and remote AET from Lua scripts ("OnStoredInstance") +* Option "DicomAssociationCloseDelay" to set delay before closing DICOM association +* ZIP archives now display the accession number of the studies + +Plugins +------- + +* Introspection of plugins (cf. the "/plugins" URI) +* Plugins can access the command-line arguments used to launch Orthanc +* Plugins can extend Orthanc Explorer with custom JavaScript +* Plugins can get/set global properties to save their configuration +* Plugins can do REST calls to other plugins (cf. "xxxAfterPlugins()") +* Scan of folders for plugins + +Fixes +----- + +* Code refactorings +* Fix issue 25 (AET with underscore not allowed) +* Fix replacement and insertion of private DICOM tags +* Fix anonymization generating non-portable DICOM files + + +Version 0.8.5 (2014/11/04) +========================== + +General +------- + +* Major speed-up thanks to a new database schema +* Plugins can monitor changes through callbacks +* Download ZIP + DICOMDIR from Orthanc Explorer +* Sample plugin framework to serve static resources (./Plugins/Samples/WebSkeleton/) + +Fixes +----- + +* Fix issue 19 (YBR_FULL are decoded incorrectly) +* Fix issue 21 (Microsoft Visual Studio precompiled headers) +* Fix issue 22 (Error decoding multi-frame instances) +* Fix issue 24 (Build fails on OSX when directory has .DS_Store files) +* Fix crash when bad HTTP credentials are provided + + +Version 0.8.4 (2014/09/19) +========================== + +* "/instances-tags" to get the tags of all the child instances of a + patient/study/series with a single REST call (bulk tags retrieval) +* Configuration/Lua to select the accepted C-Store SCP transfer syntaxes +* Fix reporting of errors in Orthanc Explorer when sending images to peers/modalities +* Installation of plugin SDK in CMake + + +Version 0.8.3 (2014/09/11) +========================== + +Major +----- + +* Creation of ZIP archives for media storage, with DICOMDIR +* URIs to get all the children of a given resource in a single REST call +* "/tools/lookup" URI to map DICOM UIDs to Orthanc identifiers +* Support of index-only mode (using the "StoreDicom" option) +* Plugins can implement a custom storage area + +Minor +----- + +* Configuration option to enable HTTP Keep-Alive +* Configuration option to disable the logging of exported resources in "/exports" +* Plugins can retrieve the path to Orthanc and to its configuration file +* "/tools/create-dicom" now accepts the "PatientID" DICOM tag (+ updated sample) +* Possibility to set HTTP headers from plugins +* "LastUpdate" metadata is now always returned for patients, studies and series + +Maintenance +----------- + +* Refactoring of HttpOutput ("Content-Length" header is now always sent) +* Upgrade to Mongoose 3.8 +* Fixes for Visual Studio 2013 and Windows 64bit +* Fix issue 16: Handling of "AT" value representations in JSON +* Fix issue 17 + + +Version 0.8.2 (2014/08/07) +========================== + +* Support of the standard text encodings +* Hot restart of Orthanc by posting to "/tools/reset" +* More fault-tolerant commands in Lua scripts +* Parameter to set the default encoding for DICOM files without SpecificCharacterSet +* Fix of issue #14 (support of XCode 5.1) +* Upgrade to Google Test 1.7.0 + + +Version 0.8.1 (2014/07/29) +========================== + +General +------- + +* Access patient module at the study level to cope with PatientID collisions +* On-the-fly conversion of JSON to XML according to the HTTP Accept header +* C-Echo SCU in the REST API +* DICOM conformance statement available at URI "/tools/dicom-conformance" + +Lua scripts +----------- + +* Lua scripts can do HTTP requests, and thus can call Web services +* Lua scripts can invoke system commands, with CallSystem() + +Plugins +------- + +* Lookup for DICOM UIDs in the plugin SDK +* Plugins have access to the HTTP headers and can answer with HTTP status codes +* Callback to react to the incoming of DICOM instances + +Fixes +----- + +* Fix build of Google Log with Visual Studio >= 11.0 +* Fix automated generation of the list of resource children in the REST API + + +Version 0.8.0 (2014/07/10) +========================== + +Major changes +------------- + +* Routing images with Lua scripts +* Introduction of the Orthanc Plugin SDK +* Official support of OS X (Darwin) 10.8 + +Minor changes +------------- + +* Extraction of tags for the patient/study/series/instance DICOM modules +* Extraction of the tags shared by all the instances of a patient/study/series * Options to limit the number of results for an incoming C-FIND query * Support of kFreeBSD +* Several code refactorings +* Fix OrthancCppClient::GetVoxelSizeZ() Version 0.7.6 (2014/06/11) @@ -21,7 +205,7 @@ * Creation of DICOM instances using the REST API * Embedding of images within DICOM instances * Adding/removal/modification of remote modalities/peers through REST -* Reuse of the previous SCU connection to avoid unecessary handshakes +* Reuse of the previous SCU connection to avoid unnecessary handshakes * Fix problems with anonymization and modification * Fix missing licensing terms about reuse of some code from DCMTK * Various code refactorings @@ -150,7 +334,7 @@ ========================== * "Bulk" Store-SCU (send several DICOM instances with the same - DICOM connexion) + DICOM connection) * Store-SCU for patients and studies in Orthanc Explorer * Filtering of incoming DICOM instances (through Lua scripting) * Filtering of incoming HTTP requests (through Lua scripting) @@ -175,7 +359,7 @@ ------------- * Download of modified or anonymized DICOM instances -* Inplace modification and anymization of DICOM series, studies and patients +* Inplace modification and anonymization of DICOM series, studies and patients Minor changes -------------
--- a/OrthancCppClient/Instance.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancCppClient/Instance.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancCppClient/Instance.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancCppClient/Instance.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancCppClient/OrthancClientException.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancCppClient/OrthancClientException.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancCppClient/OrthancConnection.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancCppClient/OrthancConnection.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancCppClient/OrthancConnection.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancCppClient/OrthancConnection.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancCppClient/OrthancCppClient.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancCppClient/OrthancCppClient.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancCppClient/Patient.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancCppClient/Patient.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancCppClient/Patient.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancCppClient/Patient.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancCppClient/Series.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancCppClient/Series.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -53,11 +53,12 @@ /** * Compute the slice normal from Image Orientation Patient. * http://nipy.sourceforge.net/nibabel/dicom/dicom_orientation.html#dicom-z-from-slice + * http://dicomiseasy.blogspot.be/2013/06/getting-oriented-using-image-plane.html * http://www.itk.org/pipermail/insight-users/2003-September/004762.html **/ std::vector<float> cosines; - someSlice.SplitVectorOfFloats(cosines, "ImageOrientationPatient"); + someSlice.SplitVectorOfFloats(cosines, "ImageOrientationPatient"); // 0020-0037 if (cosines.size() != 6) { @@ -76,7 +77,7 @@ float ComputeSliceLocation(Instance& instance) const { std::vector<float> ipp; - instance.SplitVectorOfFloats(ipp, "ImagePositionPatient"); + instance.SplitVectorOfFloats(ipp, "ImagePositionPatient"); // 0020-0032 if (ipp.size() != 3) { throw OrthancClientException(Orthanc::ErrorCode_BadFileFormat); @@ -129,7 +130,7 @@ if (instance_.GetPixelFormat() == format_) { - memcpy(p, instance_.GetBuffer(y), 2 * instance_.GetWidth()); + memcpy(p, instance_.GetBuffer(y), GetBytesPerPixel(instance_.GetPixelFormat()) * instance_.GetWidth()); } else if (instance_.GetPixelFormat() == PixelFormat_Grayscale8 && format_ == PixelFormat_RGB24) @@ -212,32 +213,77 @@ { if (GetInstanceCount() == 0) { + // Empty image, use some default value (should never happen) + voxelSizeX_ = 1; + voxelSizeY_ = 1; + voxelSizeZ_ = 1; + sliceThickness_ = 1; + return true; } - Instance& i1 = GetInstance(0); + // Choose a reference slice + Instance& reference = GetInstance(0); + // Check that all the child instances share the same 3D parameters for (unsigned int i = 0; i < GetInstanceCount(); i++) { Instance& i2 = GetInstance(i); - if (std::string(i1.GetTagAsString("Columns")) != std::string(i2.GetTagAsString("Columns")) || - std::string(i1.GetTagAsString("Rows")) != std::string(i2.GetTagAsString("Rows")) || - std::string(i1.GetTagAsString("ImageOrientationPatient")) != std::string(i2.GetTagAsString("ImageOrientationPatient")) || - std::string(i1.GetTagAsString("SliceThickness")) != std::string(i2.GetTagAsString("SliceThickness")) || - std::string(i1.GetTagAsString("PixelSpacing")) != std::string(i2.GetTagAsString("PixelSpacing"))) + if (std::string(reference.GetTagAsString("Columns")) != std::string(i2.GetTagAsString("Columns")) || + std::string(reference.GetTagAsString("Rows")) != std::string(i2.GetTagAsString("Rows")) || + std::string(reference.GetTagAsString("ImageOrientationPatient")) != std::string(i2.GetTagAsString("ImageOrientationPatient")) || + std::string(reference.GetTagAsString("SliceThickness")) != std::string(i2.GetTagAsString("SliceThickness")) || + std::string(reference.GetTagAsString("PixelSpacing")) != std::string(i2.GetTagAsString("PixelSpacing"))) { return false; } } - SliceLocator locator(GetInstance(0)); + + // Extract X/Y voxel size and slice thickness + std::string s = GetInstance(0).GetTagAsString("PixelSpacing"); // 0028-0030 + size_t pos = s.find('\\'); + assert(pos != std::string::npos); + std::string sy = s.substr(0, pos); + std::string sx = s.substr(pos + 1); + + try + { + voxelSizeX_ = boost::lexical_cast<float>(sx); + voxelSizeY_ = boost::lexical_cast<float>(sy); + } + catch (boost::bad_lexical_cast) + { + throw OrthancClientException(Orthanc::ErrorCode_BadFileFormat); + } + + sliceThickness_ = GetInstance(0).GetTagAsFloat("SliceThickness"); // 0018-0050 + + + // Compute the location of each slice to extract the voxel size along Z + voxelSizeZ_ = std::numeric_limits<float>::infinity(); + + SliceLocator locator(reference); + float referenceSliceLocation = locator.ComputeSliceLocation(reference); + std::set<float> l; for (unsigned int i = 0; i < GetInstanceCount(); i++) { - l.insert(locator.ComputeSliceLocation(GetInstance(i))); + float location = locator.ComputeSliceLocation(GetInstance(i)); + float distanceToReferenceSlice = fabs(location - referenceSliceLocation); + + l.insert(location); + + if (distanceToReferenceSlice > std::numeric_limits<float>::epsilon() && + distanceToReferenceSlice < voxelSizeZ_) + { + voxelSizeZ_ = distanceToReferenceSlice; + } } + + // Make sure that 2 slices do not share the same Z location return l.size() == GetInstanceCount(); } catch (OrthancClientException) @@ -275,10 +321,10 @@ status_ = Status3DImage_NotTested; url_ = std::string(connection_.GetOrthancUrl()) + "/series/" + id_; - isVoxelSizeRead_ = false; voxelSizeX_ = 0; voxelSizeY_ = 0; voxelSizeZ_ = 0; + sliceThickness_ = 0; instances_.SetThreadCount(connection.GetThreadCount()); } @@ -324,46 +370,6 @@ return GetInstance(0).GetTagAsInt("Rows"); } - void Series::LoadVoxelSize() - { - if (isVoxelSizeRead_) - { - return; - } - - Check3DImage(); - - if (GetInstanceCount() == 0) - { - // Empty image, use some default value - voxelSizeX_ = 1; - voxelSizeY_ = 1; - voxelSizeZ_ = 1; - } - else - { - try - { - std::string s = GetInstance(0).GetTagAsString("PixelSpacing"); - size_t pos = s.find('\\'); - assert(pos != std::string::npos); - std::string sy = s.substr(0, pos); - std::string sx = s.substr(pos + 1); - - voxelSizeX_ = boost::lexical_cast<float>(sx); - voxelSizeY_ = boost::lexical_cast<float>(sy); - voxelSizeZ_ = GetInstance(0).GetTagAsFloat("SliceThickness"); - } - catch (boost::bad_lexical_cast) - { - throw OrthancClientException(Orthanc::ErrorCode_NotImplemented); - } - } - - isVoxelSizeRead_ = true; - } - - const char* Series::GetMainDicomTag(const char* tag, const char* defaultValue) const { if (series_["MainDicomTags"].isMember(tag)) @@ -486,22 +492,28 @@ float Series::GetVoxelSizeX() { - LoadVoxelSize(); + Check3DImage(); // Is3DImageInternal() will compute the voxel sizes return voxelSizeX_; } float Series::GetVoxelSizeY() { - LoadVoxelSize(); + Check3DImage(); // Is3DImageInternal() will compute the voxel sizes return voxelSizeY_; } float Series::GetVoxelSizeZ() { - LoadVoxelSize(); + Check3DImage(); // Is3DImageInternal() will compute the voxel sizes return voxelSizeZ_; } + float Series::GetSliceThickness() + { + Check3DImage(); // Is3DImageInternal() will compute the voxel sizes + return sliceThickness_; + } + void Series::Load3DImage(void* target, Orthanc::PixelFormat format, int64_t lineStride,
--- a/OrthancCppClient/Series.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancCppClient/Series.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -62,11 +62,11 @@ Orthanc::ArrayFilledByThreads instances_; Status3DImage status_; - bool isVoxelSizeRead_; float voxelSizeX_; float voxelSizeY_; float voxelSizeZ_; - + float sliceThickness_; + void Check3DImage(); bool Is3DImageInternal(); @@ -86,8 +86,6 @@ size_t stackStride, Orthanc::ThreadedCommandProcessor::IListener* listener); - void LoadVoxelSize(); - public: /** * {summary}{Create a connection to some series.} @@ -189,6 +187,13 @@ **/ float GetVoxelSizeZ(); + /** + * {summary}{Get the slice thickness.} + * {description}{Get the slice thickness. This call is only valid if this series corresponds to a 3D image.} + * {returns}{The slice thickness.} + **/ + float GetSliceThickness(); + LAAW_API_INTERNAL void Load3DImage(void* target, Orthanc::PixelFormat format, int64_t lineStride,
--- a/OrthancCppClient/SharedLibrary/AUTOGENERATED/ExternC.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancCppClient/SharedLibrary/AUTOGENERATED/ExternC.cpp Thu May 21 16:58:30 2015 +0200 @@ -795,6 +795,29 @@ } } + LAAW_EXPORT_DLL_API char* LAAW_CALL_CONVENTION LAAW_EXTERNC_2be452e7af5bf7dfd8c5021842674497(void* thisObject, float* result) + { + try + { + #ifdef LAAW_EXTERNC_START_FUNCTION + LAAW_EXTERNC_START_FUNCTION; + #endif + + OrthancClient::Series* this_ = static_cast<OrthancClient::Series*>(thisObject); +*result = this_->GetSliceThickness(); + + return NULL; + } + catch (::Laaw::LaawException& e) + { + return LAAW_EXTERNC_CopyString(e.What()); + } + catch (...) + { + return LAAW_EXTERNC_CopyString("..."); + } + } + LAAW_EXPORT_DLL_API char* LAAW_CALL_CONVENTION LAAW_EXTERNC_4dcc7a0fd025efba251ac6e9b701c2c5(void* thisObject, void* arg0, int32_t arg1, int64_t arg2, int64_t arg3) { try @@ -1466,7 +1489,7 @@ LAAW_EXPORT_DLL_API const char* LAAW_CALL_CONVENTION LAAW_EXTERNC_GetCompany() { - return "CHU of Liege"; + return "University Hospital of Liege"; } LAAW_EXPORT_DLL_API const char* LAAW_CALL_CONVENTION LAAW_EXTERNC_GetProduct() @@ -1476,22 +1499,22 @@ LAAW_EXPORT_DLL_API const char* LAAW_CALL_CONVENTION LAAW_EXTERNC_GetCopyright() { - return "(c) 2012-2014, Sebastien Jodogne, CHU of Liege"; + return "(c) 2012-2015, Sebastien Jodogne, University Hospital of Liege"; } LAAW_EXPORT_DLL_API const char* LAAW_CALL_CONVENTION LAAW_EXTERNC_GetVersion() { - return "0.7"; + return "0.8"; } LAAW_EXPORT_DLL_API const char* LAAW_CALL_CONVENTION LAAW_EXTERNC_GetFileVersion() { - return "0.7.0.6"; + return "0.8.0.6"; } LAAW_EXPORT_DLL_API const char* LAAW_CALL_CONVENTION LAAW_EXTERNC_GetFullVersion() { - return "0.7.6"; + return "0.8.6"; } LAAW_EXPORT_DLL_API void LAAW_CALL_CONVENTION LAAW_EXTERNC_FreeString(char* str)
--- a/OrthancCppClient/SharedLibrary/AUTOGENERATED/OrthancCppClient.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancCppClient/SharedLibrary/AUTOGENERATED/OrthancCppClient.h Thu May 21 16:58:30 2015 +0200 @@ -81,9 +81,9 @@ /* cf. http://sourceforge.net/p/predef/wiki/Architectures/ */ #ifdef __amd64__ -#define LAAW_ORTHANC_CLIENT_DEFAULT_PATH "libOrthancClient.so.0.7" +#define LAAW_ORTHANC_CLIENT_DEFAULT_PATH "libOrthancClient.so.0.8" #else -#define LAAW_ORTHANC_CLIENT_DEFAULT_PATH "libOrthancClient.so.0.7" +#define LAAW_ORTHANC_CLIENT_DEFAULT_PATH "libOrthancClient.so.0.8" #endif #define LAAW_ORTHANC_CLIENT_CALL_CONV @@ -203,7 +203,7 @@ { private: LAAW_ORTHANC_CLIENT_HANDLE_TYPE handle_; - LAAW_ORTHANC_CLIENT_FUNCTION_TYPE functionsIndex_[62 + 1]; + LAAW_ORTHANC_CLIENT_FUNCTION_TYPE functionsIndex_[63 + 1]; @@ -239,7 +239,7 @@ void FreeString(char* str) { typedef void (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (char*); - Function function = (Function) GetFunction(62); + Function function = (Function) GetFunction(63); function(str); } @@ -302,7 +302,15 @@ { if (handle_ != LAAW_ORTHANC_CLIENT_HANDLE_NULL) { +#if 0 + /** + * Do not explicitly unload the shared library, as it might + * interfere with the destruction of static objects declared + * inside the library (e.g. this is the case of gflags that is + * internally used by googlelog). + **/ LAAW_ORTHANC_CLIENT_CLOSER(handle_); +#endif handle_ = LAAW_ORTHANC_CLIENT_HANDLE_NULL; } } @@ -386,12 +394,12 @@ * It is assumed that the API does not change when the revision * number (MAJOR.MINOR.REVISION) changes. **/ - if (strcmp(getVersion(), "0.7")) + if (strcmp(getVersion(), "0.8")) { throw ::OrthancClient::OrthancClientException("Mismatch between the C++ header and the library version"); } - functionsIndex_[62] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_FreeString", "4"); + functionsIndex_[63] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_FreeString", "4"); functionsIndex_[3] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_557aee7b61817292a0f31269d3c35db7", "8"); functionsIndex_[4] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_0b8dff0ce67f10954a49b059e348837e", "8"); functionsIndex_[5] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_e05097c153f676e5a5ee54dcfc78256f", "4"); @@ -423,40 +431,41 @@ functionsIndex_[30] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_b794f5cd3dad7d7b575dd1fd902afdd0", "8"); functionsIndex_[31] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_8ee2e50dd9df8f66a3c1766090dd03ab", "8"); functionsIndex_[32] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_046aed35bbe4751691f4c34cc249a61d", "8"); - functionsIndex_[33] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_4dcc7a0fd025efba251ac6e9b701c2c5", "28"); - functionsIndex_[34] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_b2601a161c24ad0a1d3586246f87452c", "32"); + functionsIndex_[33] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_2be452e7af5bf7dfd8c5021842674497", "8"); + functionsIndex_[34] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_4dcc7a0fd025efba251ac6e9b701c2c5", "28"); + functionsIndex_[35] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_b2601a161c24ad0a1d3586246f87452c", "32"); functionsIndex_[19] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_193599b9e345384fcdfcd47c29c55342", "12"); functionsIndex_[20] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_7c97f17063a357d38c5fab1136ad12a0", "4"); - functionsIndex_[37] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_e65b20b7e0170b67544cd6664a4639b7", "4"); - functionsIndex_[38] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_470e981b0e41f17231ba0ae6f3033321", "8"); - functionsIndex_[39] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_04cefd138b6ea15ad909858f2a0a8f05", "12"); - functionsIndex_[40] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_aee5b1f6f0c082f2c3b0986f9f6a18c7", "8"); - functionsIndex_[41] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_93965682bace75491413e1f0b8d5a654", "16"); - functionsIndex_[35] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_b01c6003238eb46c8db5dc823d7ca678", "12"); - functionsIndex_[36] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_0147007fb99bad8cd95a139ec8795376", "4"); - functionsIndex_[44] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_236ee8b403bc99535a8a4695c0cd45cb", "8"); - functionsIndex_[45] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_2a437b7aba6bb01e81113835be8f0146", "8"); - functionsIndex_[46] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_2bcbcb850934ae0bb4c6f0cc940e6cda", "8"); - functionsIndex_[47] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_8d415c3a78a48e7e61d9fd24e7c79484", "12"); - functionsIndex_[48] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_70d2f8398bbc63b5f792b69b4ad5fecb", "12"); - functionsIndex_[49] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_1729a067d902771517388eedd7346b23", "12"); - functionsIndex_[50] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_72e2aeee66cd3abd8ab7e987321c3745", "8"); - functionsIndex_[51] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_1ea3df5a1ac1a1a687fe7325adddb6f0", "8"); - functionsIndex_[52] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_99b4f370e4f532d8b763e2cb49db92f8", "8"); - functionsIndex_[53] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_c41c742b68617f1c0590577a0a5ebc0c", "8"); - functionsIndex_[54] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_142dd2feba0fc1d262bbd0baeb441a8b", "8"); - functionsIndex_[55] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_5f5c9f81a4dff8daa6c359f1d0488fef", "12"); - functionsIndex_[56] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_9ca979fffd08fa256306d4e68d8b0e91", "8"); - functionsIndex_[57] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_6f2d77a26edc91c28d89408dbc3c271e", "8"); - functionsIndex_[58] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_c0f494b80d4ff8b232df7a75baa0700a", "4"); - functionsIndex_[59] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_d604f44bd5195e082e745e9cbc164f4c", "4"); - functionsIndex_[60] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_1710299d1c5f3b1f2b7cf3962deebbfd", "8"); - functionsIndex_[61] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_bb55aaf772ddceaadee36f4e54136bcb", "8"); - functionsIndex_[42] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_6c5ad02f91b583e29cebd0bd319ce21d", "12"); - functionsIndex_[43] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_4068241c44a9c1367fe0e57be523f207", "4"); + functionsIndex_[38] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_e65b20b7e0170b67544cd6664a4639b7", "4"); + functionsIndex_[39] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_470e981b0e41f17231ba0ae6f3033321", "8"); + functionsIndex_[40] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_04cefd138b6ea15ad909858f2a0a8f05", "12"); + functionsIndex_[41] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_aee5b1f6f0c082f2c3b0986f9f6a18c7", "8"); + functionsIndex_[42] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_93965682bace75491413e1f0b8d5a654", "16"); + functionsIndex_[36] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_b01c6003238eb46c8db5dc823d7ca678", "12"); + functionsIndex_[37] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_0147007fb99bad8cd95a139ec8795376", "4"); + functionsIndex_[45] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_236ee8b403bc99535a8a4695c0cd45cb", "8"); + functionsIndex_[46] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_2a437b7aba6bb01e81113835be8f0146", "8"); + functionsIndex_[47] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_2bcbcb850934ae0bb4c6f0cc940e6cda", "8"); + functionsIndex_[48] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_8d415c3a78a48e7e61d9fd24e7c79484", "12"); + functionsIndex_[49] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_70d2f8398bbc63b5f792b69b4ad5fecb", "12"); + functionsIndex_[50] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_1729a067d902771517388eedd7346b23", "12"); + functionsIndex_[51] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_72e2aeee66cd3abd8ab7e987321c3745", "8"); + functionsIndex_[52] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_1ea3df5a1ac1a1a687fe7325adddb6f0", "8"); + functionsIndex_[53] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_99b4f370e4f532d8b763e2cb49db92f8", "8"); + functionsIndex_[54] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_c41c742b68617f1c0590577a0a5ebc0c", "8"); + functionsIndex_[55] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_142dd2feba0fc1d262bbd0baeb441a8b", "8"); + functionsIndex_[56] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_5f5c9f81a4dff8daa6c359f1d0488fef", "12"); + functionsIndex_[57] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_9ca979fffd08fa256306d4e68d8b0e91", "8"); + functionsIndex_[58] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_6f2d77a26edc91c28d89408dbc3c271e", "8"); + functionsIndex_[59] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_c0f494b80d4ff8b232df7a75baa0700a", "4"); + functionsIndex_[60] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_d604f44bd5195e082e745e9cbc164f4c", "4"); + functionsIndex_[61] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_1710299d1c5f3b1f2b7cf3962deebbfd", "8"); + functionsIndex_[62] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_bb55aaf772ddceaadee36f4e54136bcb", "8"); + functionsIndex_[43] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_6c5ad02f91b583e29cebd0bd319ce21d", "12"); + functionsIndex_[44] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_4068241c44a9c1367fe0e57be523f207", "4"); /* Check whether the functions were properly loaded */ - for (unsigned int i = 0; i <= 62; i++) + for (unsigned int i = 0; i <= 63; i++) { if (functionsIndex_[i] == (LAAW_ORTHANC_CLIENT_FUNCTION_TYPE) NULL) { @@ -704,6 +713,7 @@ inline float GetVoxelSizeX(); inline float GetVoxelSizeY(); inline float GetVoxelSizeZ(); + inline float GetSliceThickness(); inline void Load3DImage(void* target, ::Orthanc::PixelFormat format, LAAW_INT64 lineStride, LAAW_INT64 stackStride); inline void Load3DImage(void* target, ::Orthanc::PixelFormat format, LAAW_INT64 lineStride, LAAW_INT64 stackStride, float progress[]); }; @@ -1323,6 +1333,22 @@ return result_; } /** + * @brief Get the slice thickness. + * + * Get the slice thickness. This call is only valid if this series corresponds to a 3D image. + * + * @return The slice thickness. + **/ + inline float Series::GetSliceThickness() + { + float result_; + typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, float*); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(33); + char* error = function(pimpl_, &result_); + ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); + return result_; + } + /** * @brief Load the 3D image into a memory buffer. * * Load the 3D image into a memory buffer. This call is only valid if this series corresponds to a 3D image. The "target" buffer must be wide enough to store all the voxels of the image. @@ -1335,7 +1361,7 @@ inline void Series::Load3DImage(void* target, ::Orthanc::PixelFormat format, LAAW_INT64 lineStride, LAAW_INT64 stackStride) { typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, void*, LAAW_INT32, LAAW_INT64, LAAW_INT64); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(33); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(34); char* error = function(pimpl_, target, format, lineStride, stackStride); ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); } @@ -1353,7 +1379,7 @@ inline void Series::Load3DImage(void* target, ::Orthanc::PixelFormat format, LAAW_INT64 lineStride, LAAW_INT64 stackStride, float progress[]) { typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, void*, LAAW_INT32, LAAW_INT64, LAAW_INT64, float*); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(34); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(35); char* error = function(pimpl_, target, format, lineStride, stackStride, progress); ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); } @@ -1373,7 +1399,7 @@ { isReference_ = false; typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void**, void*, const char*); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(35); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(36); char* error = function(&pimpl_, connection.pimpl_, id.c_str()); ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); } @@ -1387,7 +1413,7 @@ { if (isReference_) return; typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(36); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(37); char* error = function(pimpl_); error = error; // Remove warning about unused variable } @@ -1400,7 +1426,7 @@ inline void Study::Reload() { typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(37); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(38); char* error = function(pimpl_); ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); } @@ -1415,7 +1441,7 @@ { LAAW_UINT32 result_; typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, LAAW_UINT32*); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(38); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(39); char* error = function(pimpl_, &result_); ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); return result_; @@ -1432,7 +1458,7 @@ { void* result_; typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, void**, LAAW_UINT32); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(39); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(40); char* error = function(pimpl_, &result_, index); ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); return ::OrthancClient::Series(result_); @@ -1448,7 +1474,7 @@ { const char* result_; typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (const void*, const char**); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(40); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(41); char* error = function(pimpl_, &result_); ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); return std::string(result_); @@ -1466,7 +1492,7 @@ { const char* result_; typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (const void*, const char**, const char*, const char*); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(41); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(42); char* error = function(pimpl_, &result_, tag.c_str(), defaultValue.c_str()); ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); return std::string(result_); @@ -1487,7 +1513,7 @@ { isReference_ = false; typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void**, void*, const char*); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(42); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(43); char* error = function(&pimpl_, connection.pimpl_, id.c_str()); ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); } @@ -1501,7 +1527,7 @@ { if (isReference_) return; typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(43); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(44); char* error = function(pimpl_); error = error; // Remove warning about unused variable } @@ -1516,7 +1542,7 @@ { const char* result_; typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (const void*, const char**); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(44); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(45); char* error = function(pimpl_, &result_); ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); return std::string(result_); @@ -1531,7 +1557,7 @@ inline void Instance::SetImageExtractionMode(::Orthanc::ImageExtractionMode mode) { typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, LAAW_INT32); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(45); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(46); char* error = function(pimpl_, mode); ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); } @@ -1546,7 +1572,7 @@ { LAAW_INT32 result_; typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (const void*, LAAW_INT32*); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(46); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(47); char* error = function(pimpl_, &result_); ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); return static_cast< ::Orthanc::ImageExtractionMode >(result_); @@ -1563,7 +1589,7 @@ { const char* result_; typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (const void*, const char**, const char*); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(47); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(48); char* error = function(pimpl_, &result_, tag.c_str()); ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); return std::string(result_); @@ -1580,7 +1606,7 @@ { float result_; typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (const void*, float*, const char*); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(48); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(49); char* error = function(pimpl_, &result_, tag.c_str()); ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); return result_; @@ -1597,7 +1623,7 @@ { LAAW_INT32 result_; typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (const void*, LAAW_INT32*, const char*); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(49); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(50); char* error = function(pimpl_, &result_, tag.c_str()); ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); return result_; @@ -1613,7 +1639,7 @@ { LAAW_UINT32 result_; typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, LAAW_UINT32*); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(50); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(51); char* error = function(pimpl_, &result_); ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); return result_; @@ -1629,7 +1655,7 @@ { LAAW_UINT32 result_; typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, LAAW_UINT32*); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(51); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(52); char* error = function(pimpl_, &result_); ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); return result_; @@ -1645,7 +1671,7 @@ { LAAW_UINT32 result_; typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, LAAW_UINT32*); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(52); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(53); char* error = function(pimpl_, &result_); ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); return result_; @@ -1661,7 +1687,7 @@ { LAAW_INT32 result_; typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, LAAW_INT32*); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(53); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(54); char* error = function(pimpl_, &result_); ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); return static_cast< ::Orthanc::PixelFormat >(result_); @@ -1677,7 +1703,7 @@ { const void* result_; typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, const void**); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(54); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(55); char* error = function(pimpl_, &result_); ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); return reinterpret_cast< const void* >(result_); @@ -1694,7 +1720,7 @@ { const void* result_; typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, const void**, LAAW_UINT32); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(55); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(56); char* error = function(pimpl_, &result_, y); ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); return reinterpret_cast< const void* >(result_); @@ -1710,7 +1736,7 @@ { LAAW_UINT64 result_; typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, LAAW_UINT64*); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(56); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(57); char* error = function(pimpl_, &result_); ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); return result_; @@ -1726,7 +1752,7 @@ { const void* result_; typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, const void**); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(57); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(58); char* error = function(pimpl_, &result_); ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); return reinterpret_cast< const void* >(result_); @@ -1740,7 +1766,7 @@ inline void Instance::DiscardImage() { typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(58); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(59); char* error = function(pimpl_); ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); } @@ -1753,7 +1779,7 @@ inline void Instance::DiscardDicom() { typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(59); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(60); char* error = function(pimpl_); ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); } @@ -1767,7 +1793,7 @@ inline void Instance::LoadTagContent(const ::std::string& path) { typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, const char*); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(60); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(61); char* error = function(pimpl_, path.c_str()); ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); } @@ -1782,7 +1808,7 @@ { const char* result_; typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (const void*, const char**); - Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(61); + Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(62); char* error = function(pimpl_, &result_); ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error); return std::string(result_);
--- a/OrthancCppClient/SharedLibrary/AUTOGENERATED/Windows32.def Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancCppClient/SharedLibrary/AUTOGENERATED/Windows32.def Thu May 21 16:58:30 2015 +0200 @@ -31,6 +31,7 @@ _LAAW_EXTERNC_b794f5cd3dad7d7b575dd1fd902afdd0@8 = LAAW_EXTERNC_b794f5cd3dad7d7b575dd1fd902afdd0@8 _LAAW_EXTERNC_8ee2e50dd9df8f66a3c1766090dd03ab@8 = LAAW_EXTERNC_8ee2e50dd9df8f66a3c1766090dd03ab@8 _LAAW_EXTERNC_046aed35bbe4751691f4c34cc249a61d@8 = LAAW_EXTERNC_046aed35bbe4751691f4c34cc249a61d@8 + _LAAW_EXTERNC_2be452e7af5bf7dfd8c5021842674497@8 = LAAW_EXTERNC_2be452e7af5bf7dfd8c5021842674497@8 _LAAW_EXTERNC_4dcc7a0fd025efba251ac6e9b701c2c5@28 = LAAW_EXTERNC_4dcc7a0fd025efba251ac6e9b701c2c5@28 _LAAW_EXTERNC_b2601a161c24ad0a1d3586246f87452c@32 = LAAW_EXTERNC_b2601a161c24ad0a1d3586246f87452c@32 _LAAW_EXTERNC_193599b9e345384fcdfcd47c29c55342@12 = LAAW_EXTERNC_193599b9e345384fcdfcd47c29c55342@12
--- a/OrthancCppClient/SharedLibrary/AUTOGENERATED/Windows32.rc Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancCppClient/SharedLibrary/AUTOGENERATED/Windows32.rc Thu May 21 16:58:30 2015 +0200 @@ -1,8 +1,8 @@ #include <winver.h> VS_VERSION_INFO VERSIONINFO - FILEVERSION 0,7,0,6 - PRODUCTVERSION 0,7,0,0 + FILEVERSION 0,8,0,6 + PRODUCTVERSION 0,8,0,0 FILEOS VOS_NT_WINDOWS32 FILETYPE VFT_DLL BEGIN @@ -10,16 +10,16 @@ BEGIN BLOCK "040904E4" BEGIN - VALUE "Comments", "Release 0.7.6" - VALUE "CompanyName", "CHU of Liege" + VALUE "Comments", "Release 0.8.6" + VALUE "CompanyName", "University Hospital of Liege" VALUE "FileDescription", "Native client to the REST API of Orthanc" - VALUE "FileVersion", "0.7.0.6" + VALUE "FileVersion", "0.8.0.6" VALUE "InternalName", "OrthancClient" - VALUE "LegalCopyright", "(c) 2012-2014, Sebastien Jodogne, CHU of Liege" - VALUE "LegalTrademarks", "Licensing information is available on https://code.google.com/p/orthanc/" + VALUE "LegalCopyright", "(c) 2012-2015, Sebastien Jodogne, University Hospital of Liege" + VALUE "LegalTrademarks", "Licensing information is available at http://www.orthanc-server.com/" VALUE "OriginalFilename", "OrthancClient_Windows32.dll" VALUE "ProductName", "OrthancClient" - VALUE "ProductVersion", "0.7" + VALUE "ProductVersion", "0.8" END END
--- a/OrthancCppClient/SharedLibrary/AUTOGENERATED/Windows64.def Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancCppClient/SharedLibrary/AUTOGENERATED/Windows64.def Thu May 21 16:58:30 2015 +0200 @@ -31,6 +31,7 @@ LAAW_EXTERNC_b794f5cd3dad7d7b575dd1fd902afdd0 LAAW_EXTERNC_8ee2e50dd9df8f66a3c1766090dd03ab LAAW_EXTERNC_046aed35bbe4751691f4c34cc249a61d + LAAW_EXTERNC_2be452e7af5bf7dfd8c5021842674497 LAAW_EXTERNC_4dcc7a0fd025efba251ac6e9b701c2c5 LAAW_EXTERNC_b2601a161c24ad0a1d3586246f87452c LAAW_EXTERNC_193599b9e345384fcdfcd47c29c55342
--- a/OrthancCppClient/SharedLibrary/AUTOGENERATED/Windows64.rc Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancCppClient/SharedLibrary/AUTOGENERATED/Windows64.rc Thu May 21 16:58:30 2015 +0200 @@ -1,8 +1,8 @@ #include <winver.h> VS_VERSION_INFO VERSIONINFO - FILEVERSION 0,7,0,6 - PRODUCTVERSION 0,7,0,0 + FILEVERSION 0,8,0,6 + PRODUCTVERSION 0,8,0,0 FILEOS VOS_NT_WINDOWS32 FILETYPE VFT_DLL BEGIN @@ -10,16 +10,16 @@ BEGIN BLOCK "040904E4" BEGIN - VALUE "Comments", "Release 0.7.6" - VALUE "CompanyName", "CHU of Liege" + VALUE "Comments", "Release 0.8.6" + VALUE "CompanyName", "University Hospital of Liege" VALUE "FileDescription", "Native client to the REST API of Orthanc" - VALUE "FileVersion", "0.7.0.6" + VALUE "FileVersion", "0.8.0.6" VALUE "InternalName", "OrthancClient" - VALUE "LegalCopyright", "(c) 2012-2014, Sebastien Jodogne, CHU of Liege" - VALUE "LegalTrademarks", "Licensing information is available on https://code.google.com/p/orthanc/" + VALUE "LegalCopyright", "(c) 2012-2015, Sebastien Jodogne, University Hospital of Liege" + VALUE "LegalTrademarks", "Licensing information is available at http://www.orthanc-server.com/" VALUE "OriginalFilename", "OrthancClient_Windows64.dll" VALUE "ProductName", "OrthancClient" - VALUE "ProductVersion", "0.7" + VALUE "ProductVersion", "0.8" END END
--- a/OrthancCppClient/SharedLibrary/Product.json Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancCppClient/SharedLibrary/Product.json Thu May 21 16:58:30 2015 +0200 @@ -1,8 +1,8 @@ { "Product" : "OrthancClient", "Description" : "Native client to the REST API of Orthanc", - "Company" : "CHU of Liege", - "Copyright" : "(c) 2012-2014, Sebastien Jodogne, CHU of Liege", - "Legal" : "Licensing information is available on https://code.google.com/p/orthanc/", - "Version" : "0.7.6" + "Company" : "University Hospital of Liege", + "Copyright" : "(c) 2012-2015, Sebastien Jodogne, University Hospital of Liege", + "Legal" : "Licensing information is available at http://www.orthanc-server.com/", + "Version" : "0.8.6" }
--- a/OrthancCppClient/SharedLibrary/SharedLibrary.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancCppClient/SharedLibrary/SharedLibrary.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancCppClient/Study.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancCppClient/Study.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancCppClient/Study.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancCppClient/Study.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancExplorer/explorer.html Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancExplorer/explorer.html Thu May 21 16:58:30 2015 +0200 @@ -30,11 +30,13 @@ <link rel="stylesheet" href="explorer.css" /> <script src="file-upload.js"></script> <script src="explorer.js"></script> + <script src="../plugins/explorer.js"></script> </head> <body> <div data-role="page" id="find-patients" > <div data-role="header" > <h1><span class="orthanc-name"></span>Find a patient</h1> + <a href="#plugins" data-icon="grid" class="ui-btn-left" data-direction="reverse">Plugins</a> <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a> </div> <div data-role="content"> @@ -46,7 +48,7 @@ <div data-role="page" id="upload" > <div data-role="header" > <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> + <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a> </div> <div data-role="content"> <div style="display:none"> @@ -72,7 +74,7 @@ <div data-role="page" id="patient" > <div data-role="header" > <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="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a> <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a> </div> <div data-role="content"> @@ -105,6 +107,7 @@ <a href="#" id="patient-modified-from">Before modification</a> </li> <li data-icon="gear"><a href="#" id="patient-archive">Download ZIP</a></li> + <li data-icon="gear"><a href="#" id="patient-media">Download DICOMDIR</a></li> </ul> </div> </div> @@ -125,7 +128,7 @@ <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="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a> <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a> </div> <div data-role="content"> @@ -151,6 +154,7 @@ <a href="#" id="study-modified-from">Before modification</a> </li> <li data-icon="gear"><a href="#" id="study-archive">Download ZIP</a></li> + <li data-icon="gear"><a href="#" id="study-media">Download DICOMDIR</a></li> </ul> </div> </div> @@ -173,7 +177,7 @@ Series </h1> - <a href="#find-patients" data-icon="search" class="ui-btn-left" data-direction="reverse">Find patient</a> + <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a> <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a> </div> <div data-role="content"> @@ -200,6 +204,7 @@ </li> <li data-icon="search"><a href="#" id="series-preview">Preview this series</a></li> <li data-icon="gear"><a href="#" id="series-archive">Download ZIP</a></li> + <li data-icon="gear"><a href="#" id="series-media">Download DICOMDIR</a></li> </ul> </div> </div> @@ -222,7 +227,7 @@ <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="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a> <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a> </div> <div data-role="content"> @@ -268,6 +273,18 @@ </div> </div> + <div data-role="page" id="plugins" > + <div data-role="header" > + <h1><span class="orthanc-name"></span>Plugins</h1> + <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a> + </div> + <div data-role="content"> + <ul id="all-plugins" data-role="listview" data-inset="true" data-filter="true"> + </ul> + </div> + </div> + + <div id="peer-store" style="display:none;" class="ui-body-c"> <p align="center"><b>Sending to Orthanc peer...</b></p> <p><img src="libs/images/ajax-loader2.gif" alt="" /></p>
--- a/OrthancExplorer/explorer.js Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancExplorer/explorer.js Thu May 21 16:58:30 2015 +0200 @@ -142,11 +142,10 @@ -function GetSingleResource(type, uuid, callback) +function GetResource(uri, callback) { - var resource = null; $.ajax({ - url: '../' + type + '/' + uuid, + url: '..' + uri, dataType: 'json', async: false, cache: false, @@ -157,42 +156,6 @@ } -function GetMultipleResources(type, uuids, callback) -{ - if (uuids == null) - { - $.ajax({ - url: '../' + type, - dataType: 'json', - async: false, - cache: false, - success: function(s) { - uuids = s; - } - }); - } - - var resources = []; - var ajaxRequests = uuids.map(function(uuid) { - return $.ajax({ - url: '../' + type + '/' + uuid, - dataType: 'json', - async: true, - cache: false, - success: function(s) { - resources.push(s); - } - }); - }); - - // Wait for all the AJAX requests to end - $.when.apply($, ajaxRequests).then(function() { - callback(resources); - }); -} - - - function CompleteFormatting(s, link, isReverse) { if (link != null) @@ -344,18 +307,18 @@ $('#find-patients').live('pagebeforeshow', function() { - GetMultipleResources('patients', null, function(patients) { - var target = $('#all-patients'); - $('li', target).remove(); + GetResource('/patients?expand', function(patients) { + var target = $('#all-patients'); + $('li', target).remove(); - SortOnDicomTag(patients, 'PatientName', false, false); + SortOnDicomTag(patients, 'PatientName', false, false); - for (var i = 0; i < patients.length; i++) { - var p = FormatPatient(patients[i], '#patient?uuid=' + patients[i].ID); - target.append(p); - } + for (var i = 0; i < patients.length; i++) { + var p = FormatPatient(patients[i], '#patient?uuid=' + patients[i].ID); + target.append(p); + } - target.listview('refresh'); + target.listview('refresh'); }); }); @@ -381,8 +344,8 @@ function RefreshPatient() { if ($.mobile.pageData) { - GetSingleResource('patients', $.mobile.pageData.uuid, function(patient) { - GetMultipleResources('studies', patient.Studies, function(studies) { + GetResource('/patients/' + $.mobile.pageData.uuid, function(patient) { + GetResource('/patients/' + $.mobile.pageData.uuid + '/studies', function(studies) { SortOnDicomTag(studies, 'StudyDate', false, true); $('#patient-info li').remove(); @@ -433,9 +396,9 @@ function RefreshStudy() { if ($.mobile.pageData) { - GetSingleResource('studies', $.mobile.pageData.uuid, function(study) { - GetSingleResource('patients', study.ParentPatient, function(patient) { - GetMultipleResources('series', study.Series, function(series) { + GetResource('/studies/' + $.mobile.pageData.uuid, function(study) { + GetResource('/patients/' + study.ParentPatient, function(patient) { + GetResource('/studies/' + $.mobile.pageData.uuid + '/series', function(series) { SortOnDicomTag(series, 'SeriesDate', false, true); $('#study .patient-link').attr('href', '#patient?uuid=' + patient.ID); @@ -465,7 +428,7 @@ currentPage = 'study'; currentUuid = $.mobile.pageData.uuid; }); - }); + }); }); } } @@ -474,10 +437,10 @@ function RefreshSeries() { if ($.mobile.pageData) { - GetSingleResource('series', $.mobile.pageData.uuid, function(series) { - GetSingleResource('studies', series.ParentStudy, function(study) { - GetSingleResource('patients', study.ParentPatient, function(patient) { - GetMultipleResources('instances', series.Instances, function(instances) { + GetResource('/series/' + $.mobile.pageData.uuid, function(series) { + GetResource('/studies/' + series.ParentStudy, function(study) { + GetResource('/patients/' + study.ParentPatient, function(patient) { + GetResource('/series/' + $.mobile.pageData.uuid + '/instances', function(instances) { Sort(instances, function(x) { return x.IndexInSeries; }, true, false); $('#series .patient-link').attr('href', '#patient?uuid=' + patient.ID); @@ -568,10 +531,10 @@ function RefreshInstance() { if ($.mobile.pageData) { - GetSingleResource('instances', $.mobile.pageData.uuid, function(instance) { - GetSingleResource('series', instance.ParentSeries, function(series) { - GetSingleResource('studies', series.ParentStudy, function(study) { - GetSingleResource('patients', study.ParentPatient, function(patient) { + GetResource('/instances/' + $.mobile.pageData.uuid, function(instance) { + GetResource('/series/' + instance.ParentSeries, function(series) { + GetResource('/studies/' + series.ParentStudy, function(study) { + GetResource('/patients/' + study.ParentPatient, function(patient) { $('#instance .patient-link').attr('href', '#patient?uuid=' + patient.ID); $('#instance .study-link').attr('href', '#study?uuid=' + study.ID); @@ -589,13 +552,8 @@ .append(FormatInstance(instance)) .listview('refresh'); - $.ajax({ - url: '../instances/' + instance.ID + '/tags', - cache: false, - dataType: 'json', - success: function(s) { - $('#dicom-tree').tree('loadData', ConvertForTree(s)); - } + GetResource('/instances/' + instance.ID + '/tags', function(s) { + $('#dicom-tree').tree('loadData', ConvertForTree(s)); }); SetupAnonymizedOrModifiedFrom('#instance-anonymized-from', instance, 'instance', 'AnonymizedFrom'); @@ -728,7 +686,7 @@ $('#instance-preview').live('click', function(e) { if ($.mobile.pageData) { - GetSingleResource('instances', $.mobile.pageData.uuid + '/frames', function(frames) { + GetResource('/instances/' + $.mobile.pageData.uuid + '/frames', function(frames) { if (frames.length == 1) { // Viewing a single-frame image @@ -759,10 +717,12 @@ } }); + + $('#series-preview').live('click', function(e) { if ($.mobile.pageData) { - GetSingleResource('series', $.mobile.pageData.uuid, function(series) { - GetMultipleResources('instances', series.Instances, function(instances) { + GetResource('/series/' + $.mobile.pageData.uuid, function(series) { + GetResource('/series/' + $.mobile.pageData.uuid + '/instances', function(instances) { Sort(instances, function(x) { return x.IndexInSeries; }, true, false); var images = []; @@ -777,7 +737,7 @@ imageFadeDuration : 1, loop : true }); - }) + }); }); } }); @@ -786,7 +746,6 @@ - function ChooseDicomModality(callback) { var clickedModality = ''; @@ -935,6 +894,24 @@ window.location.href = '../series/' + $.mobile.pageData.uuid + '/archive'; }); + +$('#patient-media').live('click', function(e) { + e.preventDefault(); //stop the browser from following + window.location.href = '../patients/' + $.mobile.pageData.uuid + '/media'; +}); + +$('#study-media').live('click', function(e) { + e.preventDefault(); //stop the browser from following + window.location.href = '../studies/' + $.mobile.pageData.uuid + '/media'; +}); + +$('#series-media').live('click', function(e) { + e.preventDefault(); //stop the browser from following + window.location.href = '../series/' + $.mobile.pageData.uuid + '/media'; +}); + + + $('#protection').live('change', function(e) { var isProtected = e.target.value == "on"; $.ajax({ @@ -1005,3 +982,46 @@ OpenAnonymizeResourceDialog('../patients/' + $.mobile.pageData.uuid, 'Anonymize this patient?'); }); + + +$('#plugins').live('pagebeforeshow', function() { + $.ajax({ + url: '../plugins', + dataType: 'json', + async: false, + cache: false, + success: function(plugins) { + var target = $('#all-plugins'); + $('li', target).remove(); + + plugins.map(function(id) { + return $.ajax({ + url: '../plugins/' + id, + dataType: 'json', + async: false, + cache: false, + success: function(plugin) { + var li = $('<li>'); + var item = li; + + if ('RootUri' in plugin) + { + item = $('<a>'); + li.append(item); + item.click(function() { + window.open(plugin.RootUri); + }); + } + + item.append($('<h1>').text(plugin.ID)); + item.append($('<p>').text(plugin.Description)); + item.append($('<span>').addClass('ui-li-count').text(plugin.Version)); + target.append(li); + } + }); + }); + + target.listview('refresh'); + } + }); +});
--- a/OrthancServer/DatabaseWrapper.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/DatabaseWrapper.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -39,6 +39,7 @@ #include <glog/logging.h> #include <stdio.h> +#include <boost/lexical_cast.hpp> namespace Orthanc { @@ -92,6 +93,35 @@ } }; + class SignalResourceDeleted : public SQLite::IScalarFunction + { + private: + IServerIndexListener& listener_; + + public: + SignalResourceDeleted(IServerIndexListener& listener) : + listener_(listener) + { + } + + virtual const char* GetName() const + { + return "SignalResourceDeleted"; + } + + virtual unsigned int GetCardinality() const + { + return 2; + } + + virtual void Compute(SQLite::FunctionContext& context) + { + ResourceType type = static_cast<ResourceType>(context.GetIntValue(1)); + ServerIndexChange change(ChangeType_Deleted, type, context.GetStringValue(0)); + listener_.SignalChange(change); + } + }; + class SignalRemainingAncestor : public SQLite::IScalarFunction { private: @@ -184,20 +214,6 @@ } } - 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) { @@ -205,38 +221,12 @@ 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; + return db_.GetLastInsertRowId(); } - bool DatabaseWrapper::LookupResource(const std::string& publicId, - int64_t& id, - ResourceType& type) + bool DatabaseWrapper::LookupResource(int64_t& id, + ResourceType& type, + const std::string& publicId) { SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT internalId, resourceType FROM Resources WHERE publicId=?"); @@ -320,16 +310,17 @@ s.Run(); } - void DatabaseWrapper::GetChildren(Json::Value& childrenPublicIds, + + void DatabaseWrapper::GetChildren(std::list<std::string>& childrenPublicIds, int64_t id) { SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE parentId=?"); s.BindInt64(0, id); - childrenPublicIds = Json::arrayValue; + childrenPublicIds.clear(); while (s.Step()) { - childrenPublicIds.append(s.ColumnString(0)); + childrenPublicIds.push_back(s.ColumnString(0)); } } @@ -342,10 +333,11 @@ s.BindInt64(0, id); s.Run(); - if (signalRemainingAncestor_->HasRemainingAncestor()) + if (signalRemainingAncestor_->HasRemainingAncestor() && + listener_ != NULL) { - listener_.SignalRemainingAncestor(signalRemainingAncestor_->GetRemainingAncestorType(), - signalRemainingAncestor_->GetRemainingAncestorId()); + listener_->SignalRemainingAncestor(signalRemainingAncestor_->GetRemainingAncestorType(), + signalRemainingAncestor_->GetRemainingAncestorId()); } } @@ -404,45 +396,6 @@ } - 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) { @@ -470,10 +423,10 @@ - void DatabaseWrapper::ListAvailableAttachments(std::list<FileContentType>& result, + void DatabaseWrapper::ListAvailableAttachments(std::list<FileContentType>& target, int64_t id) { - result.clear(); + target.clear(); SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT fileType FROM AttachedFiles WHERE id=?"); @@ -481,7 +434,7 @@ while (s.Step()) { - result.push_back(static_cast<FileContentType>(s.ColumnInt(0))); + target.push_back(static_cast<FileContentType>(s.ColumnInt(0))); } } @@ -511,18 +464,33 @@ } } - void DatabaseWrapper::SetMainDicomTags(int64_t id, - const DicomMap& tags) + + static void SetMainDicomTagsInternal(SQLite::Statement& s, + int64_t id, + const DicomTag& tag, + const std::string& value) { - DicomArray flattened(tags); - for (size_t i = 0; i < flattened.GetSize(); i++) + s.BindInt64(0, id); + s.BindInt(1, tag.GetGroup()); + s.BindInt(2, tag.GetElement()); + s.BindString(3, value); + s.Run(); + } + + + void DatabaseWrapper::SetMainDicomTag(int64_t id, + const DicomTag& tag, + const std::string& value) + { + if (tag.IsIdentifier()) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO DicomIdentifiers VALUES(?, ?, ?, ?)"); + SetMainDicomTagsInternal(s, id, tag, value); + } + else { SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)"); - s.BindInt64(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(); + SetMainDicomTagsInternal(s, id, tag, value); } } @@ -539,10 +507,19 @@ s.ColumnInt(2), s.ColumnString(3)); } + + SQLite::Statement s2(db_, SQLITE_FROM_HERE, "SELECT * FROM DicomIdentifiers WHERE id=?"); + s2.BindInt64(0, id); + while (s2.Step()) + { + map.SetValue(s2.ColumnInt(1), + s2.ColumnInt(2), + s2.ColumnString(3)); + } } - bool DatabaseWrapper::GetParentPublicId(std::string& result, + bool DatabaseWrapper::GetParentPublicId(std::string& target, int64_t id) { SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b " @@ -551,7 +528,7 @@ if (s.Step()) { - result = s.ColumnString(0); + target = s.ColumnString(0); return true; } else @@ -561,201 +538,159 @@ } - void DatabaseWrapper::GetChildrenPublicId(std::list<std::string>& result, + void DatabaseWrapper::GetChildrenPublicId(std::list<std::string>& target, 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.BindInt64(0, id); - result.clear(); + target.clear(); while (s.Step()) { - result.push_back(s.ColumnString(0)); + target.push_back(s.ColumnString(0)); } } - void DatabaseWrapper::GetChildrenInternalId(std::list<int64_t>& result, + void DatabaseWrapper::GetChildrenInternalId(std::list<int64_t>& target, 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.BindInt64(0, id); - result.clear(); + target.clear(); while (s.Step()) { - result.push_back(s.ColumnInt64(0)); + target.push_back(s.ColumnInt64(0)); } } - void DatabaseWrapper::LogChange(ChangeType changeType, - int64_t internalId, - ResourceType resourceType, - const boost::posix_time::ptime& date) + void DatabaseWrapper::LogChange(int64_t internalId, + const ServerIndexChange& change) { SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Changes VALUES(NULL, ?, ?, ?, ?)"); - s.BindInt(0, changeType); + s.BindInt(0, change.GetChangeType()); s.BindInt64(1, internalId); - s.BindInt(2, resourceType); - s.BindString(3, boost::posix_time::to_iso_string(date)); + s.BindInt(2, change.GetResourceType()); + s.BindString(3, change.GetDate()); + s.Run(); + } + + + void DatabaseWrapper::GetChangesInternal(std::list<ServerIndexChange>& target, + bool& done, + SQLite::Statement& s, + uint32_t maxResults) + { + target.clear(); + + while (target.size() < maxResults && s.Step()) + { + int64_t seq = s.ColumnInt64(0); + ChangeType changeType = static_cast<ChangeType>(s.ColumnInt(1)); + ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(3)); + const std::string& date = s.ColumnString(4); + + int64_t internalId = s.ColumnInt64(2); + std::string publicId = GetPublicId(internalId); + + target.push_back(ServerIndexChange(seq, changeType, resourceType, publicId, date)); + } + + done = !(target.size() == maxResults && s.Step()); + } + + + void DatabaseWrapper::GetChanges(std::list<ServerIndexChange>& target, + bool& done, + int64_t since, + uint32_t maxResults) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes WHERE seq>? ORDER BY seq LIMIT ?"); + s.BindInt64(0, since); + s.BindInt(1, maxResults + 1); + GetChangesInternal(target, done, s, maxResults); + } + + void DatabaseWrapper::GetLastChange(std::list<ServerIndexChange>& target) + { + bool done; // Ignored + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes ORDER BY seq DESC LIMIT 1"); + GetChangesInternal(target, done, s, 1); + } + + + void DatabaseWrapper::LogExportedResource(const ExportedResource& resource) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "INSERT INTO ExportedResources VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?)"); + + s.BindInt(0, resource.GetResourceType()); + s.BindString(1, resource.GetPublicId()); + s.BindString(2, resource.GetModality()); + s.BindString(3, resource.GetPatientId()); + s.BindString(4, resource.GetStudyInstanceUid()); + s.BindString(5, resource.GetSeriesInstanceUid()); + s.BindString(6, resource.GetSopInstanceUid()); + s.BindString(7, resource.GetDate()); 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.ColumnInt64(0); - ChangeType changeType = static_cast<ChangeType>(s.ColumnInt(1)); - int64_t internalId = s.ColumnInt64(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"] = EnumerationToString(changeType); - item["ResourceType"] = EnumerationToString(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) + void DatabaseWrapper::GetExportedResourcesInternal(std::list<ExportedResource>& target, + bool& done, + SQLite::Statement& s, + uint32_t maxResults) { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes WHERE seq>? ORDER BY seq LIMIT ?"); - s.BindInt64(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); - } - + target.clear(); - 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::GetExportedResourcesInternal(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()) + while (target.size() < maxResults && s.Step()) { int64_t seq = s.ColumnInt64(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"] = EnumerationToString(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); + ExportedResource resource(seq, + resourceType, + publicId, + s.ColumnString(3), // modality + s.ColumnString(8), // date + s.ColumnString(4), // patient ID + s.ColumnString(5), // study instance UID + s.ColumnString(6), // series instance UID + s.ColumnString(7)); // sop instance UID - 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.push_back(resource); } - target = Json::objectValue; - target["Exports"] = changes; - target["Done"] = !(changes.size() == maxResults && s.Step()); - target["Last"] = static_cast<int>(last); + done = !(target.size() == maxResults && s.Step()); } - void DatabaseWrapper::GetExportedResources(Json::Value& target, + void DatabaseWrapper::GetExportedResources(std::list<ExportedResource>& target, + bool& done, int64_t since, - unsigned int maxResults) + uint32_t maxResults) { SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM ExportedResources WHERE seq>? ORDER BY seq LIMIT ?"); s.BindInt64(0, since); s.BindInt(1, maxResults + 1); - GetExportedResourcesInternal(target, s, since, maxResults); + GetExportedResourcesInternal(target, done, s, maxResults); } - void DatabaseWrapper::GetLastExportedResource(Json::Value& target) + void DatabaseWrapper::GetLastExportedResource(std::list<ExportedResource>& target) { + bool done; // Ignored SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM ExportedResources ORDER BY seq DESC LIMIT 1"); - GetExportedResourcesInternal(target, s, 0, 1); + GetExportedResourcesInternal(target, done, s, 1); } @@ -794,30 +729,37 @@ return static_cast<uint64_t>(s.ColumnInt64(0)); } - void DatabaseWrapper::GetAllPublicIds(Json::Value& target, + void DatabaseWrapper::GetAllPublicIds(std::list<std::string>& target, ResourceType resourceType) { SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE resourceType=?"); s.BindInt(0, resourceType); - target = Json::arrayValue; + target.clear(); while (s.Step()) { - target.append(s.ColumnString(0)); + target.push_back(s.ColumnString(0)); } } + static void UpgradeDatabase(SQLite::Connection& db, + EmbeddedResources::FileResourceId script) + { + std::string upgrade; + EmbeddedResources::GetFileResource(upgrade, script); + db.BeginTransaction(); + db.Execute(upgrade); + db.CommitTransaction(); + } - DatabaseWrapper::DatabaseWrapper(const std::string& path, - IServerIndexListener& listener) : - listener_(listener) + + DatabaseWrapper::DatabaseWrapper(const std::string& path) : listener_(NULL) { db_.Open(path); Open(); } - DatabaseWrapper::DatabaseWrapper(IServerIndexListener& listener) : - listener_(listener) + DatabaseWrapper::DatabaseWrapper() : listener_(NULL) { db_.OpenInMemory(); Open(); @@ -842,7 +784,12 @@ } // Check the version of the database - std::string version = GetGlobalProperty(GlobalProperty_DatabaseSchemaVersion, "Unknown"); + std::string version; + if (!LookupGlobalProperty(version, GlobalProperty_DatabaseSchemaVersion)) + { + version = "Unknown"; + } + bool ok = false; try { @@ -851,25 +798,33 @@ /** * History of the database versions: + * - Orthanc before Orthanc 0.3.0 (inclusive) had no version + * - Version 2: only Orthanc 0.3.1 * - Version 3: from Orthanc 0.3.2 to Orthanc 0.7.2 (inclusive) - * - Version 4: from Orthanc 0.7.3 (inclusive) + * - Version 4: from Orthanc 0.7.3 to Orthanc 0.8.4 (inclusive) + * - Version 5: from Orthanc 0.8.5 (inclusive) **/ - // This version of Orthanc is only compatible with versions 3 of 4 of the DB schema - ok = (v == 3 || v == 4); + // This version of Orthanc is only compatible with versions 3, 4 and 5 of the DB schema + ok = (v == 3 || v == 4 || v == 5); if (v == 3) { LOG(WARNING) << "Upgrading database version from 3 to 4"; - std::string upgrade; - EmbeddedResources::GetFileResource(upgrade, EmbeddedResources::UPGRADE_DATABASE_3_TO_4); - db_.BeginTransaction(); - db_.Execute(upgrade); - db_.CommitTransaction(); + UpgradeDatabase(db_, EmbeddedResources::UPGRADE_DATABASE_3_TO_4); + v = 4; + } + + if (v == 4) + { + LOG(WARNING) << "Upgrading database version from 4 to 5"; + UpgradeDatabase(db_, EmbeddedResources::UPGRADE_DATABASE_4_TO_5); + v = 5; } } catch (boost::bad_lexical_cast&) { + ok = false; } if (!ok) @@ -880,7 +835,13 @@ signalRemainingAncestor_ = new Internals::SignalRemainingAncestor; db_.Register(signalRemainingAncestor_); - db_.Register(new Internals::SignalFileDeleted(listener_)); + } + + void DatabaseWrapper::SetListener(IServerIndexListener& listener) + { + listener_ = &listener; + db_.Register(new Internals::SignalFileDeleted(listener)); + db_.Register(new Internals::SignalResourceDeleted(listener)); } uint64_t DatabaseWrapper::GetResourceCount(ResourceType resourceType) @@ -967,33 +928,6 @@ } - uint64_t DatabaseWrapper::IncrementGlobalSequence(GlobalProperty property) - { - std::string oldValue; - - if (LookupGlobalProperty(oldValue, property)) - { - uint64_t oldNumber; - - try - { - oldNumber = boost::lexical_cast<uint64_t>(oldValue); - SetGlobalProperty(property, boost::lexical_cast<std::string>(oldNumber + 1)); - return oldNumber + 1; - } - catch (boost::bad_lexical_cast&) - { - throw OrthancException(ErrorCode_InternalError); - } - } - else - { - // Initialize the sequence at "1" - SetGlobalProperty(property, "1"); - return 1; - } - } - void DatabaseWrapper::ClearTable(const std::string& tableName) { @@ -1010,39 +944,61 @@ } - void DatabaseWrapper::LookupTagValue(std::list<int64_t>& result, - DicomTag tag, - const std::string& value) + void DatabaseWrapper::LookupIdentifier(std::list<int64_t>& target, + const DicomTag& tag, + const std::string& value) { + if (!tag.IsIdentifier()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT id FROM MainDicomTags WHERE tagGroup=? AND tagElement=? and value=?"); + "SELECT id FROM DicomIdentifiers WHERE tagGroup=? AND tagElement=? and value=?"); s.BindInt(0, tag.GetGroup()); s.BindInt(1, tag.GetElement()); s.BindString(2, value); - result.clear(); + target.clear(); while (s.Step()) { - result.push_back(s.ColumnInt64(0)); + target.push_back(s.ColumnInt64(0)); } } - void DatabaseWrapper::LookupTagValue(std::list<int64_t>& result, - const std::string& value) + void DatabaseWrapper::LookupIdentifier(std::list<int64_t>& target, + const std::string& value) { SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT id FROM MainDicomTags WHERE value=?"); + "SELECT id FROM DicomIdentifiers WHERE value=?"); s.BindString(0, value); - result.clear(); + target.clear(); while (s.Step()) { - result.push_back(s.ColumnInt64(0)); + target.push_back(s.ColumnInt64(0)); } } + + + void DatabaseWrapper::GetAllMetadata(std::map<MetadataType, std::string>& target, + int64_t id) + { + target.clear(); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT type, value FROM Metadata WHERE id=?"); + s.BindInt64(0, id); + + while (s.Step()) + { + MetadataType key = static_cast<MetadataType>(s.ColumnInt(0)); + target[key] = s.ColumnString(1); + } + } + }
--- a/OrthancServer/DatabaseWrapper.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/DatabaseWrapper.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -32,14 +32,10 @@ #pragma once +#include "IDatabaseWrapper.h" + #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 { @@ -53,187 +49,193 @@ * translates low-level requests into SQL statements. Mutual * exclusion MUST be implemented at a higher level. **/ - class DatabaseWrapper + class DatabaseWrapper : public IDatabaseWrapper { private: - IServerIndexListener& listener_; + IServerIndexListener* listener_; SQLite::Connection db_; Internals::SignalRemainingAncestor* signalRemainingAncestor_; void Open(); - void GetChangesInternal(Json::Value& target, + void GetChangesInternal(std::list<ServerIndexChange>& target, + bool& done, SQLite::Statement& s, - int64_t since, - unsigned int maxResults); + uint32_t maxResults); - void GetExportedResourcesInternal(Json::Value& target, + void GetExportedResourcesInternal(std::list<ExportedResource>& target, + bool& done, SQLite::Statement& s, - int64_t since, - unsigned int maxResults); + uint32_t maxResults); + + void ClearTable(const std::string& tableName); public: - void SetGlobalProperty(GlobalProperty property, - const std::string& value); + DatabaseWrapper(const std::string& path); - bool LookupGlobalProperty(std::string& target, - GlobalProperty property); + DatabaseWrapper(); + + virtual void SetListener(IServerIndexListener& listener); - std::string GetGlobalProperty(GlobalProperty property, - const std::string& defaultValue = ""); + virtual void SetGlobalProperty(GlobalProperty property, + const std::string& value); - int64_t CreateResource(const std::string& publicId, - ResourceType type); + virtual bool LookupGlobalProperty(std::string& target, + GlobalProperty property); + + virtual int64_t CreateResource(const std::string& publicId, + ResourceType type); - bool LookupResource(const std::string& publicId, - int64_t& id, - ResourceType& type); + virtual bool LookupResource(int64_t& id, + ResourceType& type, + const std::string& publicId); - bool LookupParent(int64_t& parentId, - int64_t resourceId); + virtual bool LookupParent(int64_t& parentId, + int64_t resourceId); - std::string GetPublicId(int64_t resourceId); + virtual std::string GetPublicId(int64_t resourceId); - ResourceType GetResourceType(int64_t resourceId); + virtual ResourceType GetResourceType(int64_t resourceId); - void AttachChild(int64_t parent, - int64_t child); + virtual void AttachChild(int64_t parent, + int64_t child); - void GetChildren(Json::Value& childrenPublicIds, - int64_t id); + virtual void DeleteResource(int64_t id); - void DeleteResource(int64_t id); + virtual void SetMetadata(int64_t id, + MetadataType type, + const std::string& value); - void SetMetadata(int64_t id, - MetadataType type, - const std::string& value); + virtual void DeleteMetadata(int64_t id, + MetadataType type); - void DeleteMetadata(int64_t id, - MetadataType type); + virtual bool LookupMetadata(std::string& target, + int64_t id, + MetadataType type); - bool LookupMetadata(std::string& target, - int64_t id, - MetadataType type); - - void ListAvailableMetadata(std::list<MetadataType>& target, - int64_t id); + virtual void ListAvailableMetadata(std::list<MetadataType>& target, + int64_t id); - std::string GetMetadata(int64_t id, - MetadataType type, - const std::string& defaultValue = ""); + virtual void AddAttachment(int64_t id, + const FileInfo& attachment); - bool GetMetadataAsInteger(int& result, - int64_t id, - MetadataType type); + virtual void DeleteAttachment(int64_t id, + FileContentType attachment); + + virtual void ListAvailableAttachments(std::list<FileContentType>& target, + int64_t id); - void AddAttachment(int64_t id, - const FileInfo& attachment); + virtual bool LookupAttachment(FileInfo& attachment, + int64_t id, + FileContentType contentType); - void DeleteAttachment(int64_t id, - FileContentType attachment); + virtual void SetMainDicomTag(int64_t id, + const DicomTag& tag, + const std::string& value); - void ListAvailableAttachments(std::list<FileContentType>& result, + virtual void GetMainDicomTags(DicomMap& map, int64_t id); - bool LookupAttachment(FileInfo& attachment, - int64_t id, - FileContentType contentType); + virtual void GetChildrenPublicId(std::list<std::string>& target, + int64_t id); - void SetMainDicomTags(int64_t id, - const DicomMap& tags); + virtual void GetChildrenInternalId(std::list<int64_t>& target, + int64_t id); - void GetMainDicomTags(DicomMap& map, - int64_t id); - - bool GetParentPublicId(std::string& result, - int64_t id); + virtual void LogChange(int64_t internalId, + const ServerIndexChange& change); - void GetChildrenPublicId(std::list<std::string>& result, - int64_t id); + virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/, + bool& done /*out*/, + int64_t since, + uint32_t maxResults); - void GetChildrenInternalId(std::list<int64_t>& result, - int64_t id); + virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/); - 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); + virtual void LogExportedResource(const ExportedResource& resource); + + virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/, + bool& done /*out*/, + int64_t since, + uint32_t maxResults); - 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); + virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/); - void GetLastExportedResource(Json::Value& target); - - // For unit testing only! - int64_t GetTableRecordCount(const std::string& table); - - uint64_t GetTotalCompressedSize(); + virtual uint64_t GetTotalCompressedSize(); - uint64_t GetTotalUncompressedSize(); + virtual uint64_t GetTotalUncompressedSize(); - uint64_t GetResourceCount(ResourceType resourceType); + virtual uint64_t GetResourceCount(ResourceType resourceType); - void GetAllPublicIds(Json::Value& target, - ResourceType resourceType); + virtual void GetAllPublicIds(std::list<std::string>& target, + ResourceType resourceType); - bool SelectPatientToRecycle(int64_t& internalId); - - bool SelectPatientToRecycle(int64_t& internalId, - int64_t patientIdToAvoid); + virtual bool SelectPatientToRecycle(int64_t& internalId); - bool IsProtectedPatient(int64_t internalId); + virtual bool SelectPatientToRecycle(int64_t& internalId, + int64_t patientIdToAvoid); - void SetProtectedPatient(int64_t internalId, - bool isProtected); + virtual bool IsProtectedPatient(int64_t internalId); - DatabaseWrapper(const std::string& path, - IServerIndexListener& listener); + virtual void SetProtectedPatient(int64_t internalId, + bool isProtected); - DatabaseWrapper(IServerIndexListener& listener); - - SQLite::Transaction* StartTransaction() + virtual SQLite::ITransaction* StartTransaction() { return new SQLite::Transaction(db_); } + virtual void FlushToDisk() + { + db_.FlushToDisk(); + } + + virtual bool HasFlushToDisk() const + { + return true; + } + + virtual void ClearChanges() + { + ClearTable("Changes"); + } + + virtual void ClearExportedResources() + { + ClearTable("ExportedResources"); + } + + virtual bool IsExistingResource(int64_t internalId); + + virtual void LookupIdentifier(std::list<int64_t>& target, + const DicomTag& tag, + const std::string& value); + + virtual void LookupIdentifier(std::list<int64_t>& target, + const std::string& value); + + virtual void GetAllMetadata(std::map<MetadataType, std::string>& target, + int64_t id); + + + + + /** + * The methods declared below are for unit testing only! + **/ + const char* GetErrorMessage() const { return db_.GetErrorMessage(); } - void FlushToDisk() - { - db_.FlushToDisk(); - } - - uint64_t IncrementGlobalSequence(GlobalProperty property); - - void ClearTable(const std::string& tableName); + void GetChildren(std::list<std::string>& childrenPublicIds, + int64_t id); - bool IsExistingResource(int64_t internalId); + int64_t GetTableRecordCount(const std::string& table); + + bool GetParentPublicId(std::string& target, + int64_t id); - void LookupTagValue(std::list<int64_t>& result, - DicomTag tag, - const std::string& value); - - void LookupTagValue(std::list<int64_t>& result, - const std::string& value); }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/DicomDirWriter.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,560 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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/>. + **/ + + + + + +/*========================================================================= + + This file is based on portions of the following project: + + Program: DCMTK 3.6.0 + Module: http://dicom.offis.de/dcmtk.php.en + +Copyright (C) 1994-2011, OFFIS e.V. +All rights reserved. + +This software and supporting documentation were developed by + + OFFIS e.V. + R&D Division Health + Escherweg 2 + 26121 Oldenburg, Germany + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +- Neither the name of OFFIS nor the names of its contributors may be + used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +=========================================================================*/ + + + +/*** + + Validation: + + # sudo apt-get install dicom3tools + # dciodvfy DICOMDIR 2>&1 | less + # dcentvfy DICOMDIR 2>&1 | less + + http://www.dclunie.com/dicom3tools/dciodvfy.html + + DICOMDIR viewer working with Wine under Linux: + http://www.microdicom.com/ + + ***/ + + +#include "PrecompiledHeadersServer.h" +#include "DicomDirWriter.h" + +#include "FromDcmtkBridge.h" +#include "ToDcmtkBridge.h" + +#include "../Core/OrthancException.h" +#include "../Core/Uuid.h" + +#include <dcmtk/dcmdata/dcdicdir.h> +#include <dcmtk/dcmdata/dcmetinf.h> +#include <dcmtk/dcmdata/dcdeftag.h> +#include <dcmtk/dcmdata/dcuid.h> +#include <dcmtk/dcmdata/dcddirif.h> +#include <dcmtk/dcmdata/dcvrui.h> +#include <dcmtk/dcmdata/dcsequen.h> +#include <dcmtk/dcmdata/dcostrmf.h> +#include "dcmtk/dcmdata/dcvrda.h" /* for class DcmDate */ +#include "dcmtk/dcmdata/dcvrtm.h" /* for class DcmTime */ + +#include <memory> +#include <glog/logging.h> + +namespace Orthanc +{ + class DicomDirWriter::PImpl + { + private: + std::string fileSetId_; + Toolbox::TemporaryFile file_; + std::auto_ptr<DcmDicomDir> dir_; + + typedef std::pair<ResourceType, std::string> IndexKey; + typedef std::map<IndexKey, DcmDirectoryRecord* > Index; + Index index_; + + + /******************************************************************************* + * Functions adapted from "dcmdata/libsrc/dcddirif.cc" from DCMTK 3.6.0 + *******************************************************************************/ + + // print an error message to the console (stderr) that something went wrong with an attribute + static void printAttributeErrorMessage(const DcmTagKey &key, + const OFCondition &error, + const char *operation) + { + if (error.bad()) + { + OFString str; + if (operation != NULL) + { + str = "cannot "; + str += operation; + str += " "; + } + LOG(ERROR) << error.text() << ": " << str << DcmTag(key).getTagName() << " " << key; + } + } + + // copy element from dataset to directory record + static void copyElement(DcmItem& dataset, + const DcmTagKey &key, + DcmDirectoryRecord& record, + const OFBool optional, + const OFBool copyEmpty) + { + /* check whether tag exists in source dataset (if optional) */ + if (!optional || (copyEmpty && dataset.tagExists(key)) || dataset.tagExistsWithValue(key)) + { + DcmElement *delem = NULL; + /* get copy of element from source dataset */ + OFCondition status = dataset.findAndGetElement(key, delem, OFFalse /*searchIntoSub*/, OFTrue /*createCopy*/); + if (status.good()) + { + /* ... and insert it into the destination dataset (record) */ + status = record.insert(delem, OFTrue /*replaceOld*/); + if (status.good()) + { + DcmTag tag(key); + /* check for correct VR in the dataset */ + if (delem->getVR() != tag.getEVR()) + { + /* create warning message */ + LOG(WARNING) << "DICOMDIR: possibly wrong VR: " + << tag.getTagName() << " " << key << " with " + << DcmVR(delem->getVR()).getVRName() << " found, expected " + << tag.getVRName() << " instead"; + } + } else + delete delem; + } else if (status == EC_TagNotFound) + status = record.insertEmptyElement(key); + printAttributeErrorMessage(key, status, "insert"); + } + } + + // copy optional string value from dataset to directory record + static void copyStringWithDefault(DcmItem& dataset, + const DcmTagKey &key, + DcmDirectoryRecord& record, + const char *defaultValue, + const OFBool printWarning) + { + OFCondition status; + if (dataset.tagExistsWithValue(key)) + { + OFString stringValue; + /* retrieve string value from source dataset and put it into the destination dataset */ + status = dataset.findAndGetOFStringArray(key, stringValue); + if (status.good()) + status = record.putAndInsertString(key, stringValue.c_str()); + } else { + if (printWarning && (defaultValue != NULL)) + { + /* create warning message */ + LOG(WARNING) << "DICOMDIR: " << DcmTag(key).getTagName() << " " + << key << " missing, using alternative: " << defaultValue; + } + /* put default value */ + status = record.putAndInsertString(key, defaultValue); + } + } + + // create alternative study date if absent in dataset + static OFString &alternativeStudyDate(DcmItem& dataset, + OFString &result) + { + /* use another date if present */ + if (dataset.findAndGetOFStringArray(DCM_SeriesDate, result).bad() || result.empty()) + { + if (dataset.findAndGetOFStringArray(DCM_AcquisitionDate, result).bad() || result.empty()) + { + if (dataset.findAndGetOFStringArray(DCM_ContentDate, result).bad() || result.empty()) + { + /* use current date, "19000101" in case of error */ + DcmDate::getCurrentDate(result); + } + } + } + return result; + } + + + // create alternative study time if absent in dataset + static OFString &alternativeStudyTime(DcmItem& dataset, + OFString &result) + { + /* use another time if present */ + if (dataset.findAndGetOFStringArray(DCM_SeriesTime, result).bad() || result.empty()) + { + if (dataset.findAndGetOFStringArray(DCM_AcquisitionTime, result).bad() || result.empty()) + { + if (dataset.findAndGetOFStringArray(DCM_ContentTime, result).bad() || result.empty()) + { + /* use current time, "0000" in case of error */ + DcmTime::getCurrentTime(result); + } + } + } + return result; + } + + + static void copyElementType1(DcmItem& dataset, + const DcmTagKey &key, + DcmDirectoryRecord& record) + { + copyElement(dataset, key, record, OFFalse /*optional*/, OFFalse /*copyEmpty*/); + } + + static void copyElementType1C(DcmItem& dataset, + const DcmTagKey &key, + DcmDirectoryRecord& record) + { + copyElement(dataset, key, record, OFTrue /*optional*/, OFFalse /*copyEmpty*/); + } + + static void copyElementType2(DcmItem& dataset, + const DcmTagKey &key, + DcmDirectoryRecord& record) + { + copyElement(dataset, key, record, OFFalse /*optional*/, OFTrue /*copyEmpty*/); + } + + /******************************************************************************* + * End of functions adapted from "dcmdata/libsrc/dcddirif.cc" from DCMTK 3.6.0 + *******************************************************************************/ + + + DcmDicomDir& GetDicomDir() + { + if (dir_.get() == NULL) + { + dir_.reset(new DcmDicomDir(file_.GetPath().c_str(), + fileSetId_.c_str())); + } + + return *dir_; + } + + + DcmDirectoryRecord& GetRoot() + { + return GetDicomDir().getRootRecord(); + } + + + public: + PImpl() : fileSetId_("ORTHANC_MEDIA") + { + } + + void FillPatient(DcmDirectoryRecord& record, + DcmItem& dicom) + { + // cf. "DicomDirInterface::buildPatientRecord()" + + copyElementType1C(dicom, DCM_PatientID, record); + copyElementType2(dicom, DCM_PatientName, record); + } + + void FillStudy(DcmDirectoryRecord& record, + DcmItem& dicom) + { + // cf. "DicomDirInterface::buildStudyRecord()" + + OFString tmpString; + /* copy attribute values from dataset to study record */ + copyStringWithDefault(dicom, DCM_StudyDate, record, + alternativeStudyDate(dicom, tmpString).c_str(), OFTrue /*printWarning*/); + copyStringWithDefault(dicom, DCM_StudyTime, record, + alternativeStudyTime(dicom, tmpString).c_str(), OFTrue /*printWarning*/); + copyElementType2(dicom, DCM_StudyDescription, record); + copyElementType1(dicom, DCM_StudyInstanceUID, record); + /* use type 1C instead of 1 in order to avoid unwanted overwriting */ + copyElementType1C(dicom, DCM_StudyID, record); + copyElementType2(dicom, DCM_AccessionNumber, record); + } + + void FillSeries(DcmDirectoryRecord& record, + DcmItem& dicom) + { + // cf. "DicomDirInterface::buildSeriesRecord()" + + /* copy attribute values from dataset to series record */ + copyElementType1(dicom, DCM_Modality, record); + copyElementType1(dicom, DCM_SeriesInstanceUID, record); + /* use type 1C instead of 1 in order to avoid unwanted overwriting */ + copyElementType1C(dicom, DCM_SeriesNumber, record); + } + + void FillInstance(DcmDirectoryRecord& record, + DcmItem& dicom, + DcmMetaInfo& metaInfo, + const char* path) + { + // cf. "DicomDirInterface::buildImageRecord()" + + /* copy attribute values from dataset to image record */ + copyElementType1(dicom, DCM_InstanceNumber, record); + //copyElementType1C(dicom, DCM_ImageType, record); + copyElementType1C(dicom, DCM_ReferencedImageSequence, record); + + OFString tmp; + + DcmElement* item = record.remove(DCM_ReferencedImageSequence); + if (item != NULL) + { + delete item; + } + + if (record.putAndInsertString(DCM_ReferencedFileID, path).bad() || + dicom.findAndGetOFStringArray(DCM_SOPClassUID, tmp).bad() || + record.putAndInsertString(DCM_ReferencedSOPClassUIDInFile, tmp.c_str()).bad() || + dicom.findAndGetOFStringArray(DCM_SOPInstanceUID, tmp).bad() || + record.putAndInsertString(DCM_ReferencedSOPInstanceUIDInFile, tmp.c_str()).bad() || + metaInfo.findAndGetOFStringArray(DCM_TransferSyntaxUID, tmp).bad() || + record.putAndInsertString(DCM_ReferencedTransferSyntaxUIDInFile, tmp.c_str()).bad()) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + + + + bool CreateResource(DcmDirectoryRecord*& target, + ResourceType level, + DcmFileFormat& dicom, + const char* filename, + const char* path) + { + DcmDataset& dataset = *dicom.getDataset(); + + OFCondition result; + OFString id; + E_DirRecType type; + + switch (level) + { + case ResourceType_Patient: + result = dataset.findAndGetOFString(DCM_PatientID, id); + type = ERT_Patient; + break; + + case ResourceType_Study: + result = dataset.findAndGetOFString(DCM_StudyInstanceUID, id); + type = ERT_Study; + break; + + case ResourceType_Series: + result = dataset.findAndGetOFString(DCM_SeriesInstanceUID, id); + type = ERT_Series; + break; + + case ResourceType_Instance: + result = dataset.findAndGetOFString(DCM_SOPInstanceUID, id); + type = ERT_Image; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + if (!result.good()) + { + throw OrthancException(ErrorCode_InternalError); + } + + IndexKey key = std::make_pair(level, std::string(id.c_str())); + Index::iterator it = index_.find(key); + + if (it != index_.end()) + { + target = it->second; + return false; // Already existing + } + + std::auto_ptr<DcmDirectoryRecord> record(new DcmDirectoryRecord(type, NULL, filename)); + + switch (level) + { + case ResourceType_Patient: + FillPatient(*record, dataset); + break; + + case ResourceType_Study: + FillStudy(*record, dataset); + break; + + case ResourceType_Series: + FillSeries(*record, dataset); + break; + + case ResourceType_Instance: + FillInstance(*record, dataset, *dicom.getMetaInfo(), path); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + if (record->isAffectedBySpecificCharacterSet()) + { + copyElementType1C(dataset, DCM_SpecificCharacterSet, *record); + } + + target = record.get(); + GetRoot().insertSub(record.release()); + index_[key] = target; + + return true; // Newly created + } + + void Read(std::string& s) + { + if (!GetDicomDir().write(DICOMDIR_DEFAULT_TRANSFERSYNTAX, + EET_UndefinedLength /*encodingType*/, + EGL_withoutGL /*groupLength*/).good()) + { + throw OrthancException(ErrorCode_InternalError); + } + + file_.Read(s); + } + + void SetFileSetId(const std::string& id) + { + dir_.reset(NULL); + fileSetId_ = id; + } + }; + + + DicomDirWriter::DicomDirWriter() : pimpl_(new PImpl) + { + } + + DicomDirWriter::~DicomDirWriter() + { + if (pimpl_) + { + delete pimpl_; + } + } + + void DicomDirWriter::SetFileSetId(const std::string& id) + { + pimpl_->SetFileSetId(id); + } + + void DicomDirWriter::Add(const std::string& directory, + const std::string& filename, + ParsedDicomFile& dicom) + { + std::string path; + if (directory.empty()) + { + path = filename; + } + else + { + if (directory[directory.length() - 1] == '/' || + directory[directory.length() - 1] == '\\') + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + path = directory + '\\' + filename; + } + + DcmFileFormat& fileFormat = *reinterpret_cast<DcmFileFormat*>(dicom.GetDcmtkObject()); + + DcmDirectoryRecord* instance; + bool isNewInstance = pimpl_->CreateResource(instance, ResourceType_Instance, fileFormat, filename.c_str(), path.c_str()); + if (isNewInstance) + { + DcmDirectoryRecord* series; + bool isNewSeries = pimpl_->CreateResource(series, ResourceType_Series, fileFormat, filename.c_str(), NULL); + series->insertSub(instance); + + if (isNewSeries) + { + DcmDirectoryRecord* study; + bool isNewStudy = pimpl_->CreateResource(study, ResourceType_Study, fileFormat, filename.c_str(), NULL); + study->insertSub(series); + + if (isNewStudy) + { + DcmDirectoryRecord* patient; + pimpl_->CreateResource(patient, ResourceType_Patient, fileFormat, filename.c_str(), NULL); + patient->insertSub(study); + } + } + } + } + + void DicomDirWriter::Encode(std::string& target) + { + pimpl_->Read(target); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/DicomDirWriter.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,61 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "ParsedDicomFile.h" + +#include <boost/noncopyable.hpp> + +namespace Orthanc +{ + class DicomDirWriter : public boost::noncopyable + { + private: + class PImpl; + PImpl* pimpl_; + + public: + DicomDirWriter(); + + ~DicomDirWriter(); + + void SetFileSetId(const std::string& id); + + void Add(const std::string& directory, + const std::string& filename, + ParsedDicomFile& dicom); + + void Encode(std::string& target); + }; + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/DicomFindQuery.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,360 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "PrecompiledHeadersServer.h" +#include "DicomFindQuery.h" + +#include "FromDcmtkBridge.h" + +#include <boost/regex.hpp> + + +namespace Orthanc +{ + class DicomFindQuery::ValueConstraint : public DicomFindQuery::IConstraint + { + private: + bool isCaseSensitive_; + std::string expected_; + + public: + ValueConstraint(const std::string& value, + bool caseSensitive) : + isCaseSensitive_(caseSensitive), + expected_(value) + { + } + + const std::string& GetValue() const + { + return expected_; + } + + virtual bool IsExactConstraint() const + { + return isCaseSensitive_; + } + + virtual bool Apply(const std::string& value) const + { + if (isCaseSensitive_) + { + return expected_ == value; + } + else + { + std::string v, c; + Toolbox::ToLowerCase(v, value); + Toolbox::ToLowerCase(c, expected_); + return v == c; + } + } + }; + + + class DicomFindQuery::ListConstraint : public DicomFindQuery::IConstraint + { + private: + std::set<std::string> values_; + + public: + ListConstraint(const std::string& values) + { + std::vector<std::string> items; + Toolbox::TokenizeString(items, values, '\\'); + + for (size_t i = 0; i < items.size(); i++) + { + std::string lower; + Toolbox::ToLowerCase(lower, items[i]); + values_.insert(lower); + } + } + + virtual bool Apply(const std::string& value) const + { + std::string tmp; + Toolbox::ToLowerCase(tmp, value); + return values_.find(tmp) != values_.end(); + } + }; + + + class DicomFindQuery::RangeConstraint : public DicomFindQuery::IConstraint + { + private: + std::string lower_; + std::string upper_; + + public: + RangeConstraint(const std::string& range) + { + size_t separator = range.find('-'); + Toolbox::ToLowerCase(lower_, range.substr(0, separator)); + Toolbox::ToLowerCase(upper_, range.substr(separator + 1)); + } + + virtual bool Apply(const std::string& value) const + { + std::string v; + Toolbox::ToLowerCase(v, value); + + if (lower_.size() == 0 && + upper_.size() == 0) + { + return false; + } + + if (lower_.size() == 0) + { + return v <= upper_; + } + + if (upper_.size() == 0) + { + return v >= lower_; + } + + return (v >= lower_ && v <= upper_); + } + }; + + + class DicomFindQuery::WildcardConstraint : public DicomFindQuery::IConstraint + { + private: + boost::regex pattern_; + + public: + WildcardConstraint(const std::string& wildcard) + { + pattern_ = boost::regex(Toolbox::WildcardToRegularExpression(wildcard), + boost::regex::icase /* case insensitive search */); + } + + virtual bool Apply(const std::string& value) const + { + return boost::regex_match(value, pattern_); + } + }; + + + void DicomFindQuery::PrepareMainDicomTags(ResourceType level) + { + std::set<DicomTag> tags; + DicomMap::GetMainDicomTags(tags, level); + + for (std::set<DicomTag>::const_iterator + it = tags.begin(); it != tags.end(); ++it) + { + mainDicomTags_[*it] = level; + } + } + + + DicomFindQuery::DicomFindQuery() : + level_(ResourceType_Patient), + filterJson_(false) + { + PrepareMainDicomTags(ResourceType_Patient); + PrepareMainDicomTags(ResourceType_Study); + PrepareMainDicomTags(ResourceType_Series); + PrepareMainDicomTags(ResourceType_Instance); + } + + + DicomFindQuery::~DicomFindQuery() + { + for (Constraints::iterator it = constraints_.begin(); + it != constraints_.end(); it++) + { + delete it->second; + } + } + + + + + void DicomFindQuery::AssignConstraint(const DicomTag& tag, + IConstraint* constraint) + { + Constraints::iterator it = constraints_.find(tag); + + if (it != constraints_.end()) + { + constraints_.erase(it); + } + + constraints_[tag] = constraint; + + MainDicomTags::const_iterator tmp = mainDicomTags_.find(tag); + if (tmp == mainDicomTags_.end()) + { + // The query depends upon a DICOM tag that is not a main tag + // from the point of view of Orthanc, we need to decode the + // JSON file on the disk. + filterJson_ = true; + } + else + { + filteredLevels_.insert(tmp->second); + } + } + + + void DicomFindQuery::SetConstraint(const DicomTag& tag, + const std::string& constraint) + { + // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained + // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html + + if (constraint.find('-') != std::string::npos) + { + AssignConstraint(tag, new RangeConstraint(constraint)); + } + else if (constraint.find('\\') != std::string::npos) + { + AssignConstraint(tag, new ListConstraint(constraint)); + } + else if (constraint.find('*') != std::string::npos || + constraint.find('?') != std::string::npos) + { + AssignConstraint(tag, new WildcardConstraint(constraint)); + } + else + { + /** + * Case-insensitive match for PN value representation (Patient + * Name). Case-senstive match for all the other value + * representations. + * + * Reference: DICOM PS 3.4 + * - C.2.2.2.1 ("Single Value Matching") + * - C.2.2.2.4 ("Wild Card Matching") + * http://medical.nema.org/Dicom/2011/11_04pu.pdf + * + * "Except for Attributes with a PN Value Representation, only + * entities with values which match exactly the value specified in the + * request shall match. This matching is case-sensitive, i.e., + * sensitive to the exact encoding of the key attribute value in + * character sets where a letter may have multiple encodings (e.g., + * based on its case, its position in a word, or whether it is + * accented) + * + * For Attributes with a PN Value Representation (e.g., Patient Name + * (0010,0010)), an application may perform literal matching that is + * either case-sensitive, or that is insensitive to some or all + * aspects of case, position, accent, or other character encoding + * variants." + * + * (0008,0018) UI SOPInstanceUID => Case-sensitive + * (0008,0050) SH AccessionNumber => Case-sensitive + * (0010,0020) LO PatientID => Case-sensitive + * (0020,000D) UI StudyInstanceUID => Case-sensitive + * (0020,000E) UI SeriesInstanceUID => Case-sensitive + **/ + + AssignConstraint(tag, new ValueConstraint(constraint, FromDcmtkBridge::IsPNValueRepresentation(tag))); + } + } + + + bool DicomFindQuery::RestrictIdentifier(std::string& value, + DicomTag identifier) const + { + Constraints::const_iterator it = constraints_.find(identifier); + if (it == constraints_.end() || + !it->second->IsExactConstraint()) + { + return false; + } + else + { + value = dynamic_cast<ValueConstraint*>(it->second)->GetValue(); + return true; + } + } + + bool DicomFindQuery::HasMainDicomTagsFilter(ResourceType level) const + { + return filteredLevels_.find(level) != filteredLevels_.end(); + } + + bool DicomFindQuery::FilterMainDicomTags(const std::string& resourceId, + ResourceType level, + const DicomMap& mainTags) const + { + std::set<DicomTag> tags; + mainTags.GetTags(tags); + + for (std::set<DicomTag>::const_iterator + it = tags.begin(); it != tags.end(); ++it) + { + Constraints::const_iterator constraint = constraints_.find(*it); + if (constraint != constraints_.end() && + !constraint->second->Apply(mainTags.GetValue(*it).AsString())) + { + return false; + } + } + + return true; + } + + bool DicomFindQuery::HasInstanceFilter() const + { + return filterJson_; + } + + bool DicomFindQuery::FilterInstance(const std::string& instanceId, + const Json::Value& content) const + { + for (Constraints::const_iterator it = constraints_.begin(); + it != constraints_.end(); ++it) + { + std::string tag = it->first.Format(); + std::string value; + if (content.isMember(tag)) + { + value = content.get(tag, Json::arrayValue).get("Value", "").asString(); + } + + if (!it->second->Apply(value)) + { + return false; + } + } + + return true; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/DicomFindQuery.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,110 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "ResourceFinder.h" + +namespace Orthanc +{ + class DicomFindQuery : public ResourceFinder::IQuery + { + private: + class IConstraint : public boost::noncopyable + { + public: + virtual ~IConstraint() + { + } + + virtual bool IsExactConstraint() const + { + return false; + } + + virtual bool Apply(const std::string& value) const = 0; + }; + + + class ValueConstraint; + class RangeConstraint; + class ListConstraint; + class WildcardConstraint; + + typedef std::map<DicomTag, IConstraint*> Constraints; + typedef std::map<DicomTag, ResourceType> MainDicomTags; + + MainDicomTags mainDicomTags_; + ResourceType level_; + bool filterJson_; + Constraints constraints_; + std::set<ResourceType> filteredLevels_; + + void AssignConstraint(const DicomTag& tag, + IConstraint* constraint); + + void PrepareMainDicomTags(ResourceType level); + + + public: + DicomFindQuery(); + + virtual ~DicomFindQuery(); + + void SetLevel(ResourceType level) + { + level_ = level; + } + + virtual ResourceType GetLevel() const + { + return level_; + } + + void SetConstraint(const DicomTag& tag, + const std::string& constraint); + + virtual bool RestrictIdentifier(std::string& value, + DicomTag identifier) const; + + virtual bool HasMainDicomTagsFilter(ResourceType level) const; + + virtual bool FilterMainDicomTags(const std::string& resourceId, + ResourceType level, + const DicomMap& mainTags) const; + + virtual bool HasInstanceFilter() const; + + virtual bool FilterInstance(const std::string& instanceId, + const Json::Value& content) const; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/DicomInstanceToStore.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,174 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "DicomInstanceToStore.h" + +#include "FromDcmtkBridge.h" + +#include <dcmtk/dcmdata/dcfilefo.h> +#include <glog/logging.h> + + +namespace Orthanc +{ + static DcmDataset& GetDataset(ParsedDicomFile& file) + { + return *reinterpret_cast<DcmFileFormat*>(file.GetDcmtkObject())->getDataset(); + } + + + void DicomInstanceToStore::AddMetadata(ResourceType level, + MetadataType metadata, + const std::string& value) + { + metadata_[std::make_pair(level, metadata)] = value; + } + + + void DicomInstanceToStore::ComputeMissingInformation() + { + if (buffer_.HasContent() && + summary_.HasContent() && + json_.HasContent()) + { + // Fine, everything is available + return; + } + + if (!buffer_.HasContent()) + { + if (!parsed_.HasContent()) + { + throw OrthancException(ErrorCode_NotImplemented); + } + else + { + // Serialize the parsed DICOM file + buffer_.Allocate(); + if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer_.GetContent(), GetDataset(parsed_.GetContent()))) + { + LOG(ERROR) << "Unable to serialize a DICOM file to a memory buffer"; + throw OrthancException(ErrorCode_InternalError); + } + } + } + + if (summary_.HasContent() && + json_.HasContent()) + { + return; + } + + // At this point, we know that the DICOM file is available as a + // memory buffer, but that its summary or its JSON version is + // missing + + if (!parsed_.HasContent()) + { + parsed_.TakeOwnership(new ParsedDicomFile(buffer_.GetConstContent())); + } + + // At this point, we have parsed the DICOM file + + if (!summary_.HasContent()) + { + summary_.Allocate(); + FromDcmtkBridge::Convert(summary_.GetContent(), GetDataset(parsed_.GetContent())); + } + + if (!json_.HasContent()) + { + json_.Allocate(); + FromDcmtkBridge::ToJson(json_.GetContent(), GetDataset(parsed_.GetContent())); + } + } + + + + const char* DicomInstanceToStore::GetBufferData() + { + ComputeMissingInformation(); + + if (!buffer_.HasContent()) + { + throw OrthancException(ErrorCode_InternalError); + } + + if (buffer_.GetConstContent().size() == 0) + { + return NULL; + } + else + { + return buffer_.GetConstContent().c_str(); + } + } + + + size_t DicomInstanceToStore::GetBufferSize() + { + ComputeMissingInformation(); + + if (!buffer_.HasContent()) + { + throw OrthancException(ErrorCode_InternalError); + } + + return buffer_.GetConstContent().size(); + } + + + const DicomMap& DicomInstanceToStore::GetSummary() + { + ComputeMissingInformation(); + + if (!summary_.HasContent()) + { + throw OrthancException(ErrorCode_InternalError); + } + + return summary_.GetConstContent(); + } + + + const Json::Value& DicomInstanceToStore::GetJson() + { + ComputeMissingInformation(); + + if (!json_.HasContent()) + { + throw OrthancException(ErrorCode_InternalError); + } + + return json_.GetConstContent(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/DicomInstanceToStore.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,215 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "ParsedDicomFile.h" +#include "ServerIndex.h" +#include "../Core/OrthancException.h" + +namespace Orthanc +{ + class DicomInstanceToStore + { + private: + template <typename T> + class SmartContainer + { + private: + T* content_; + bool toDelete_; + bool isReadOnly_; + + void Deallocate() + { + if (content_ && toDelete_) + { + delete content_; + toDelete_ = false; + content_ = NULL; + } + } + + public: + SmartContainer() : content_(NULL), toDelete_(false), isReadOnly_(true) + { + } + + ~SmartContainer() + { + Deallocate(); + } + + void Allocate() + { + Deallocate(); + content_ = new T; + toDelete_ = true; + isReadOnly_ = false; + } + + void TakeOwnership(T* content) + { + if (content == NULL) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + Deallocate(); + content_ = content; + toDelete_ = true; + isReadOnly_ = false; + } + + void SetReference(T& content) // Read and write assign, without transfering ownership + { + Deallocate(); + content_ = &content; + toDelete_ = false; + isReadOnly_ = false; + } + + void SetConstReference(const T& content) // Read-only assign, without transfering ownership + { + Deallocate(); + content_ = &const_cast<T&>(content); + toDelete_ = false; + isReadOnly_ = true; + } + + bool HasContent() const + { + return content_ != NULL; + } + + T& GetContent() + { + if (content_ == NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + if (isReadOnly_) + { + throw OrthancException(ErrorCode_ReadOnly); + } + + return *content_; + } + + const T& GetConstContent() const + { + if (content_ == NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + return *content_; + } + }; + + + SmartContainer<std::string> buffer_; + SmartContainer<ParsedDicomFile> parsed_; + SmartContainer<DicomMap> summary_; + SmartContainer<Json::Value> json_; + + std::string remoteAet_; + std::string calledAet_; + ServerIndex::MetadataMap metadata_; + + void ComputeMissingInformation(); + + public: + void SetBuffer(const std::string& dicom) + { + buffer_.SetConstReference(dicom); + } + + void SetParsedDicomFile(ParsedDicomFile& parsed) + { + parsed_.SetReference(parsed); + } + + void SetSummary(const DicomMap& summary) + { + summary_.SetConstReference(summary); + } + + void SetJson(const Json::Value& json) + { + json_.SetConstReference(json); + } + + const std::string& GetRemoteAet() const + { + return remoteAet_; + } + + void SetRemoteAet(const std::string& aet) + { + remoteAet_ = aet; + } + + const std::string& GetCalledAet() const + { + return calledAet_; + } + + void SetCalledAet(const std::string& aet) + { + calledAet_ = aet; + } + + void AddMetadata(ResourceType level, + MetadataType metadata, + const std::string& value); + + const ServerIndex::MetadataMap& GetMetadata() const + { + return metadata_; + } + + ServerIndex::MetadataMap& GetMetadata() + { + return metadata_; + } + + const char* GetBufferData(); + + size_t GetBufferSize(); + + const DicomMap& GetSummary(); + + const Json::Value& GetJson(); + }; +}
--- a/OrthancServer/DicomModification.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/DicomModification.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -39,8 +39,23 @@ #include <memory> // For std::auto_ptr #include <glog/logging.h> + +static const std::string ORTHANC_DEIDENTIFICATION_METHOD = "Orthanc " ORTHANC_VERSION " - PS 3.15-2008 Table E.1-1"; + namespace Orthanc { + void DicomModification::MarkNotOrthancAnonymization() + { + Replacements::iterator it = replacements_.find(DICOM_TAG_DEIDENTIFICATION_METHOD); + + if (it != replacements_.end() && + it->second == ORTHANC_DEIDENTIFICATION_METHOD) + { + replacements_.erase(it); + } + } + + void DicomModification::MapDicomIdentifier(ParsedDicomFile& dicom, ResourceType level) { @@ -90,18 +105,29 @@ { removePrivateTags_ = false; level_ = ResourceType_Instance; + allowManualIdentifiers_ = true; } void DicomModification::Keep(const DicomTag& tag) { removals_.erase(tag); replacements_.erase(tag); + + if (FromDcmtkBridge::IsPrivateTag(tag)) + { + privateTagsToKeep_.insert(tag); + } + + MarkNotOrthancAnonymization(); } void DicomModification::Remove(const DicomTag& tag) { removals_.insert(tag); replacements_.erase(tag); + privateTagsToKeep_.erase(tag); + + MarkNotOrthancAnonymization(); } bool DicomModification::IsRemoved(const DicomTag& tag) const @@ -110,10 +136,17 @@ } void DicomModification::Replace(const DicomTag& tag, - const std::string& value) + const std::string& value, + bool safeForAnonymization) { removals_.erase(tag); + privateTagsToKeep_.erase(tag); replacements_[tag] = value; + + if (!safeForAnonymization) + { + MarkNotOrthancAnonymization(); + } } bool DicomModification::IsReplaced(const DicomTag& tag) const @@ -138,12 +171,22 @@ void DicomModification::SetRemovePrivateTags(bool removed) { removePrivateTags_ = removed; + + if (!removed) + { + MarkNotOrthancAnonymization(); + } } void DicomModification::SetLevel(ResourceType level) { uidMap_.clear(); level_ = level; + + if (level != ResourceType_Patient) + { + MarkNotOrthancAnonymization(); + } } void DicomModification::SetupAnonymization() @@ -153,6 +196,7 @@ removePrivateTags_ = true; level_ = ResourceType_Patient; uidMap_.clear(); + privateTagsToKeep_.clear(); // This is Table E.1-1 from PS 3.15-2008 - DICOM Part 15: Security and System Management Profiles removals_.insert(DicomTag(0x0008, 0x0014)); // Instance Creator UID @@ -211,7 +255,7 @@ removals_.insert(DicomTag(0x0010, 0x2000)); // Medical Alerts // Set the DeidentificationMethod tag - replacements_.insert(std::make_pair(DicomTag(0x0012, 0x0063), "Orthanc " ORTHANC_VERSION " - PS 3.15-2008 Table E.1-1")); + replacements_.insert(std::make_pair(DICOM_TAG_DEIDENTIFICATION_METHOD, ORTHANC_DEIDENTIFICATION_METHOD)); // Set the PatientIdentityRemoved tag replacements_.insert(std::make_pair(DicomTag(0x0012, 0x0062), "YES")); @@ -236,36 +280,111 @@ { throw OrthancException(ErrorCode_BadRequest); } + + // Sanity checks at the patient level if (level_ == ResourceType_Patient && !IsReplaced(DICOM_TAG_PATIENT_ID)) { LOG(ERROR) << "When modifying a patient, her PatientID is required to be modified"; throw OrthancException(ErrorCode_BadRequest); } - if (level_ > ResourceType_Patient && IsReplaced(DICOM_TAG_PATIENT_ID)) + if (!allowManualIdentifiers_) { + if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID)) + { + LOG(ERROR) << "When modifying a patient, the StudyInstanceUID cannot be manually modified"; + throw OrthancException(ErrorCode_BadRequest); + } + + if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID)) + { + LOG(ERROR) << "When modifying a patient, the SeriesInstanceUID cannot be manually modified"; + throw OrthancException(ErrorCode_BadRequest); + } + + if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID)) + { + LOG(ERROR) << "When modifying a patient, the SopInstanceUID cannot be manually modified"; + throw OrthancException(ErrorCode_BadRequest); + } + } + + + // Sanity checks at the study level + if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_PATIENT_ID)) + { + LOG(ERROR) << "When modifying a study, the parent PatientID cannot be manually modified"; throw OrthancException(ErrorCode_BadRequest); } - if (level_ > ResourceType_Study && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID)) + if (!allowManualIdentifiers_) { + if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID)) + { + LOG(ERROR) << "When modifying a study, the SeriesInstanceUID cannot be manually modified"; + throw OrthancException(ErrorCode_BadRequest); + } + + if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID)) + { + LOG(ERROR) << "When modifying a study, the SopInstanceUID cannot be manually modified"; + throw OrthancException(ErrorCode_BadRequest); + } + } + + + // Sanity checks at the series level + if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_PATIENT_ID)) + { + LOG(ERROR) << "When modifying a series, the parent PatientID cannot be manually modified"; + throw OrthancException(ErrorCode_BadRequest); + } + + if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID)) + { + LOG(ERROR) << "When modifying a series, the parent StudyInstanceUID cannot be manually modified"; throw OrthancException(ErrorCode_BadRequest); } - if (level_ > ResourceType_Series && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID)) + if (!allowManualIdentifiers_) { + if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID)) + { + LOG(ERROR) << "When modifying a series, the SopInstanceUID cannot be manually modified"; + throw OrthancException(ErrorCode_BadRequest); + } + } + + + // Sanity checks at the instance level + if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_PATIENT_ID)) + { + LOG(ERROR) << "When modifying an instance, the parent PatientID cannot be manually modified"; throw OrthancException(ErrorCode_BadRequest); } + if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID)) + { + LOG(ERROR) << "When modifying an instance, the parent StudyInstanceUID cannot be manually modified"; + throw OrthancException(ErrorCode_BadRequest); + } + + if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID)) + { + LOG(ERROR) << "When modifying an instance, the parent SeriesInstanceUID cannot be manually modified"; + throw OrthancException(ErrorCode_BadRequest); + } + + // (1) Remove the private tags, if need be if (removePrivateTags_) { - toModify.RemovePrivateTags(); + toModify.RemovePrivateTags(privateTagsToKeep_); } // (2) Remove the tags specified by the user - for (Removals::const_iterator it = removals_.begin(); + for (SetOfTags::const_iterator it = removals_.begin(); it != removals_.end(); ++it) { toModify.Remove(*it); @@ -279,17 +398,20 @@ } // (4) Update the DICOM identifiers - if (level_ <= ResourceType_Study) + if (level_ <= ResourceType_Study && + !IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID)) { MapDicomIdentifier(toModify, ResourceType_Study); } - if (level_ <= ResourceType_Series) + if (level_ <= ResourceType_Series && + !IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID)) { MapDicomIdentifier(toModify, ResourceType_Series); } - if (level_ <= ResourceType_Instance) // Always true + if (level_ <= ResourceType_Instance && // Always true + !IsReplaced(DICOM_TAG_SOP_INSTANCE_UID)) { MapDicomIdentifier(toModify, ResourceType_Instance); }
--- a/OrthancServer/DicomModification.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/DicomModification.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -46,19 +46,23 @@ **/ private: - typedef std::set<DicomTag> Removals; + typedef std::set<DicomTag> SetOfTags; typedef std::map<DicomTag, std::string> Replacements; typedef std::map< std::pair<ResourceType, std::string>, std::string> UidMap; - Removals removals_; + SetOfTags removals_; Replacements replacements_; bool removePrivateTags_; ResourceType level_; UidMap uidMap_; + SetOfTags privateTagsToKeep_; + bool allowManualIdentifiers_; void MapDicomIdentifier(ParsedDicomFile& dicom, ResourceType level); + void MarkNotOrthancAnonymization(); + public: DicomModification(); @@ -69,7 +73,8 @@ bool IsRemoved(const DicomTag& tag) const; void Replace(const DicomTag& tag, - const std::string& value); + const std::string& value, + bool safeForAnonymization = false); bool IsReplaced(const DicomTag& tag) const; @@ -92,5 +97,15 @@ void SetupAnonymization(); void Apply(ParsedDicomFile& toModify); + + void SetAllowManualIdentifiers(bool check) + { + allowManualIdentifiers_ = check; + } + + bool AreAllowManualIdentifiers() const + { + return allowManualIdentifiers_; + } }; }
--- a/OrthancServer/DicomProtocol/DicomFindAnswers.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/DicomProtocol/DicomFindAnswers.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancServer/DicomProtocol/DicomFindAnswers.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/DicomProtocol/DicomFindAnswers.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancServer/DicomProtocol/DicomServer.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/DicomProtocol/DicomServer.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -190,6 +190,11 @@ LOG(INFO) << "DICOM server stopping"; + if (server->isThreaded_) + { + server->bagOfDispatchers_.StopAll(); + } + /* drop the network, i.e. free memory of T_ASC_Network* structure. This call */ /* is the counterpart of ASC_initializeNetwork(...) which was called above. */ cond = ASC_dropNetwork(&net); @@ -273,11 +278,20 @@ throw OrthancException("Too short AET"); } + if (aet.size() > 16) + { + throw OrthancException("AET must be shorter than 16 characters"); + } + for (size_t i = 0; i < aet.size(); i++) { - if (!isalnum(aet[i]) && aet[i] != '-') + if (!(aet[i] == '-' || + aet[i] == '_' || + isdigit(aet[i]) || + (aet[i] >= 'A' && aet[i] <= 'Z'))) { - throw OrthancException("Only alphanumeric characters are allowed in AET"); + LOG(WARNING) << "For best interoperability, only upper case, alphanumeric characters should be present in AET: \"" << aet << "\""; + break; } } @@ -403,8 +417,6 @@ { pimpl_->thread_.join(); } - - bagOfDispatchers_.StopAll(); } bool DicomServer::IsMyAETitle(const std::string& aet) const
--- a/OrthancServer/DicomProtocol/DicomServer.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/DicomProtocol/DicomServer.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancServer/DicomProtocol/DicomUserConnection.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/DicomProtocol/DicomUserConnection.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -135,6 +135,8 @@ struct DicomUserConnection::PImpl { // Connection state + uint32_t dimseTimeout_; + uint32_t acseTimeout_; T_ASC_Network* net_; T_ASC_Parameters* params_; T_ASC_Association* assoc_; @@ -154,7 +156,8 @@ { if (cond.bad()) { - throw OrthancException("DicomUserConnection: " + std::string(cond.text())); + LOG(ERROR) << "DicomUserConnection: " << std::string(cond.text()); + throw OrthancException(ErrorCode_NetworkProtocol); } } @@ -162,7 +165,8 @@ { if (!IsOpen()) { - throw OrthancException("DicomUserConnection: First open the connection"); + LOG(ERROR) << "DicomUserConnection: First open the connection"; + throw OrthancException(ErrorCode_NetworkProtocol); } } @@ -323,7 +327,7 @@ DcmDataset* statusDetail = NULL; Check(DIMSE_storeUser(assoc_, presID, &req, NULL, dcmff.getDataset(), /*progressCallback*/ NULL, NULL, - /*opt_blockMode*/ DIMSE_BLOCKING, /*opt_dimse_timeout*/ 0, + /*opt_blockMode*/ DIMSE_BLOCKING, /*opt_dimse_timeout*/ dimseTimeout_, &rsp, &statusDetail, NULL)); if (statusDetail != NULL) @@ -450,7 +454,7 @@ int presID = ASC_findAcceptedPresentationContextID(pimpl_->assoc_, sopClass); if (presID == 0) { - throw OrthancException("DicomUserConnection: The C-FIND command is not supported by the distant AET"); + throw OrthancException("DicomUserConnection: The C-FIND command is not supported by the remote AET"); } T_DIMSE_C_FindRQ request; @@ -464,7 +468,8 @@ DcmDataset* statusDetail = NULL; OFCondition cond = DIMSE_findUser(pimpl_->assoc_, presID, &request, dataset.get(), FindCallback, &result, - /*opt_blockMode*/ DIMSE_BLOCKING, /*opt_dimse_timeout*/ 0, + /*opt_blockMode*/ DIMSE_BLOCKING, + /*opt_dimse_timeout*/ pimpl_->dimseTimeout_, &response, &statusDetail); if (statusDetail) @@ -541,7 +546,7 @@ int presID = ASC_findAcceptedPresentationContextID(pimpl_->assoc_, sopClass); if (presID == 0) { - throw OrthancException("DicomUserConnection: The C-MOVE command is not supported by the distant AET"); + throw OrthancException("DicomUserConnection: The C-MOVE command is not supported by the remote AET"); } T_DIMSE_C_MoveRQ request; @@ -557,7 +562,8 @@ DcmDataset* responseIdentifiers = NULL; OFCondition cond = DIMSE_moveUser(pimpl_->assoc_, presID, &request, dataset.get(), NULL, NULL, - /*opt_blockMode*/ DIMSE_BLOCKING, /*opt_dimse_timeout*/ 0, + /*opt_blockMode*/ DIMSE_BLOCKING, + /*opt_dimse_timeout*/ pimpl_->dimseTimeout_, pimpl_->net_, NULL, NULL, &response, &statusDetail, &responseIdentifiers); @@ -608,12 +614,13 @@ pimpl_(new PImpl), preferredTransferSyntax_(DEFAULT_PREFERRED_TRANSFER_SYNTAX), localAet_("STORESCU"), - distantAet_("ANY-SCP"), - distantHost_("127.0.0.1") + remoteAet_("ANY-SCP"), + remoteHost_("127.0.0.1") { - distantPort_ = 104; + remotePort_ = 104; manufacturer_ = ModalityManufacturer_Generic; + SetTimeout(10); pimpl_->net_ = NULL; pimpl_->params_ = NULL; pimpl_->assoc_ = NULL; @@ -635,10 +642,10 @@ void DicomUserConnection::Connect(const RemoteModalityParameters& parameters) { - SetDistantApplicationEntityTitle(parameters.GetApplicationEntityTitle()); - SetDistantHost(parameters.GetHost()); - SetDistantPort(parameters.GetPort()); - SetDistantManufacturer(parameters.GetManufacturer()); + SetRemoteApplicationEntityTitle(parameters.GetApplicationEntityTitle()); + SetRemoteHost(parameters.GetHost()); + SetRemotePort(parameters.GetPort()); + SetRemoteManufacturer(parameters.GetManufacturer()); } @@ -651,16 +658,16 @@ } } - void DicomUserConnection::SetDistantApplicationEntityTitle(const std::string& aet) + void DicomUserConnection::SetRemoteApplicationEntityTitle(const std::string& aet) { - if (distantAet_ != aet) + if (remoteAet_ != aet) { Close(); - distantAet_ = aet; + remoteAet_ = aet; } } - void DicomUserConnection::SetDistantManufacturer(ModalityManufacturer manufacturer) + void DicomUserConnection::SetRemoteManufacturer(ModalityManufacturer manufacturer) { if (manufacturer_ != manufacturer) { @@ -684,26 +691,26 @@ } - void DicomUserConnection::SetDistantHost(const std::string& host) + void DicomUserConnection::SetRemoteHost(const std::string& host) { - if (distantHost_ != host) + if (remoteHost_ != host) { if (host.size() > HOST_NAME_MAX - 10) { - throw OrthancException("Distant host name is too long"); + throw OrthancException("Remote host name is too long"); } Close(); - distantHost_ = host; + remoteHost_ = host; } } - void DicomUserConnection::SetDistantPort(uint16_t port) + void DicomUserConnection::SetRemotePort(uint16_t port) { - if (distantPort_ != port) + if (remotePort_ != port) { Close(); - distantPort_ = port; + remotePort_ = port; } } @@ -716,30 +723,30 @@ } LOG(INFO) << "Opening a DICOM SCU connection from AET \"" << GetLocalApplicationEntityTitle() - << "\" to AET \"" << GetDistantApplicationEntityTitle() << "\" on host " - << GetDistantHost() << ":" << GetDistantPort() - << " (manufacturer: " << EnumerationToString(GetDistantManufacturer()) << ")"; + << "\" to AET \"" << GetRemoteApplicationEntityTitle() << "\" on host " + << GetRemoteHost() << ":" << GetRemotePort() + << " (manufacturer: " << EnumerationToString(GetRemoteManufacturer()) << ")"; - Check(ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ 30, &pimpl_->net_)); + Check(ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ pimpl_->acseTimeout_, &pimpl_->net_)); Check(ASC_createAssociationParameters(&pimpl_->params_, /*opt_maxReceivePDULength*/ ASC_DEFAULTMAXPDU)); // Set this application's title and the called application's title in the params - Check(ASC_setAPTitles(pimpl_->params_, localAet_.c_str(), distantAet_.c_str(), NULL)); + Check(ASC_setAPTitles(pimpl_->params_, localAet_.c_str(), remoteAet_.c_str(), NULL)); - // Set the network addresses of the local and distant entities + // Set the network addresses of the local and remote entities char localHost[HOST_NAME_MAX]; gethostname(localHost, HOST_NAME_MAX - 1); - char distantHostAndPort[HOST_NAME_MAX]; + char remoteHostAndPort[HOST_NAME_MAX]; #ifdef _MSC_VER _snprintf #else snprintf #endif - (distantHostAndPort, HOST_NAME_MAX - 1, "%s:%d", distantHost_.c_str(), distantPort_); + (remoteHostAndPort, HOST_NAME_MAX - 1, "%s:%d", remoteHost_.c_str(), remotePort_); - Check(ASC_setPresentationAddresses(pimpl_->params_, localHost, distantHostAndPort)); + Check(ASC_setPresentationAddresses(pimpl_->params_, localHost, remoteHostAndPort)); // Set various options Check(ASC_setTransportLayerType(pimpl_->params_, /*opt_secureConnection*/ false)); @@ -816,7 +823,8 @@ CheckIsOpen(); DIC_US status; Check(DIMSE_echoUser(pimpl_->assoc_, pimpl_->assoc_->nextMsgID++, - /*opt_blockMode*/ DIMSE_BLOCKING, /*opt_dimse_timeout*/ 0, + /*opt_blockMode*/ DIMSE_BLOCKING, + /*opt_dimse_timeout*/ pimpl_->dimseTimeout_, &status, NULL)); return status == STATUS_Success; } @@ -863,9 +871,29 @@ Move(targetAet, map); } - void DicomUserConnection::SetConnectionTimeout(uint32_t seconds) + + void DicomUserConnection::SetTimeout(uint32_t seconds) { + if (seconds <= 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + dcmConnectionTimeout.set(seconds); + pimpl_->dimseTimeout_ = seconds; + pimpl_->acseTimeout_ = 10; + } + + + void DicomUserConnection::DisableTimeout() + { + /** + * Global timeout (seconds) for connecting to remote hosts. + * Default value is -1 which selects infinite timeout, i.e. blocking connect(). + */ + dcmConnectionTimeout.set(-1); + pimpl_->dimseTimeout_ = 0; + pimpl_->acseTimeout_ = 10; } @@ -914,7 +942,7 @@ defaultStorageSOPClasses_.size() >= MAXIMUM_STORAGE_SOP_CLASSES) { // Make room in the default storage syntaxes - assert(defaultStorageSOPClasses_.size() > 0); // Necessarily true because condition (*) is false + assert(!defaultStorageSOPClasses_.empty()); // Necessarily true because condition (*) is false defaultStorageSOPClasses_.erase(*defaultStorageSOPClasses_.rbegin()); }
--- a/OrthancServer/DicomProtocol/DicomUserConnection.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/DicomProtocol/DicomUserConnection.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -60,9 +60,9 @@ // Connection parameters std::string preferredTransferSyntax_; std::string localAet_; - std::string distantAet_; - std::string distantHost_; - uint16_t distantPort_; + std::string remoteAet_; + std::string remoteHost_; + uint16_t remotePort_; ModalityManufacturer manufacturer_; std::set<std::string> storageSOPClasses_; std::list<std::string> reservedStorageSOPClasses_; @@ -97,30 +97,30 @@ return localAet_; } - void SetDistantApplicationEntityTitle(const std::string& aet); + void SetRemoteApplicationEntityTitle(const std::string& aet); - const std::string& GetDistantApplicationEntityTitle() const + const std::string& GetRemoteApplicationEntityTitle() const { - return distantAet_; + return remoteAet_; } - void SetDistantHost(const std::string& host); + void SetRemoteHost(const std::string& host); - const std::string& GetDistantHost() const + const std::string& GetRemoteHost() const { - return distantHost_; + return remoteHost_; } - void SetDistantPort(uint16_t port); + void SetRemotePort(uint16_t port); - uint16_t GetDistantPort() const + uint16_t GetRemotePort() const { - return distantPort_; + return remotePort_; } - void SetDistantManufacturer(ModalityManufacturer manufacturer); + void SetRemoteManufacturer(ModalityManufacturer manufacturer); - ModalityManufacturer GetDistantManufacturer() const + ModalityManufacturer GetRemoteManufacturer() const { return manufacturer_; } @@ -177,6 +177,8 @@ const std::string& seriesUid, const std::string& instanceUid); - static void SetConnectionTimeout(uint32_t seconds); + void SetTimeout(uint32_t seconds); + + void DisableTimeout(); }; }
--- a/OrthancServer/DicomProtocol/IApplicationEntityFilter.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/DicomProtocol/IApplicationEntityFilter.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -51,5 +51,9 @@ virtual bool IsAllowedRequest(const std::string& callingIp, const std::string& callingAet, DicomRequestType type) = 0; + + virtual bool IsAllowedTransferSyntax(const std::string& callingIp, + const std::string& callingAet, + TransferSyntax syntax) = 0; }; }
--- a/OrthancServer/DicomProtocol/IFindRequestHandler.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/DicomProtocol/IFindRequestHandler.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancServer/DicomProtocol/IFindRequestHandlerFactory.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/DicomProtocol/IFindRequestHandlerFactory.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancServer/DicomProtocol/IMoveRequestHandler.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/DicomProtocol/IMoveRequestHandler.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancServer/DicomProtocol/IMoveRequestHandlerFactory.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/DicomProtocol/IMoveRequestHandlerFactory.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancServer/DicomProtocol/IStoreRequestHandler.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/DicomProtocol/IStoreRequestHandler.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -50,6 +50,7 @@ virtual void Handle(const std::string& dicomFile, const DicomMap& dicomSummary, const Json::Value& dicomJson, - const std::string& distantAet) = 0; + const std::string& remoteAet, + const std::string& calledAet) = 0; }; }
--- a/OrthancServer/DicomProtocol/IStoreRequestHandlerFactory.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/DicomProtocol/IStoreRequestHandlerFactory.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancServer/DicomProtocol/RemoteModalityParameters.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/DicomProtocol/RemoteModalityParameters.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancServer/DicomProtocol/RemoteModalityParameters.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/DicomProtocol/RemoteModalityParameters.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancServer/DicomProtocol/ReusableDicomUserConnection.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/DicomProtocol/ReusableDicomUserConnection.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -50,10 +50,10 @@ ModalityManufacturer manufacturer) { if (connection_ != NULL && - connection_->GetDistantApplicationEntityTitle() == remoteAet && - connection_->GetDistantHost() == address && - connection_->GetDistantPort() == port && - connection_->GetDistantManufacturer() == manufacturer) + connection_->GetRemoteApplicationEntityTitle() == remoteAet && + connection_->GetRemoteHost() == address && + connection_->GetRemotePort() == port && + connection_->GetRemoteManufacturer() == manufacturer) { // The current connection can be reused LOG(INFO) << "Reusing the previous SCU connection"; @@ -64,10 +64,10 @@ connection_ = new DicomUserConnection(); connection_->SetLocalApplicationEntityTitle(localAet_); - connection_->SetDistantApplicationEntityTitle(remoteAet); - connection_->SetDistantHost(address); - connection_->SetDistantPort(port); - connection_->SetDistantManufacturer(manufacturer); + connection_->SetRemoteApplicationEntityTitle(remoteAet); + connection_->SetRemoteHost(address); + connection_->SetRemotePort(port); + connection_->SetRemoteManufacturer(manufacturer); connection_->Open(); } @@ -152,9 +152,15 @@ Close(); } - void ReusableDicomUserConnection::SetMillisecondsBeforeClose(unsigned int ms) + void ReusableDicomUserConnection::SetMillisecondsBeforeClose(uint64_t ms) { boost::mutex::scoped_lock lock(mutex_); + + if (ms == 0) + { + ms = 1; + } + timeBeforeClose_ = boost::posix_time::milliseconds(ms); } @@ -172,6 +178,14 @@ void ReusableDicomUserConnection::Unlock() { + if (connection_ != NULL && + connection_->GetRemoteManufacturer() == ModalityManufacturer_StoreScp) + { + // "storescp" from DCMTK has problems when reusing a + // connection. Always close. + Close(); + } + lastUse_ = Now(); mutex_.unlock(); }
--- a/OrthancServer/DicomProtocol/ReusableDicomUserConnection.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/DicomProtocol/ReusableDicomUserConnection.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -88,12 +88,12 @@ virtual ~ReusableDicomUserConnection(); - unsigned int GetMillisecondsBeforeClose() const + uint64_t GetMillisecondsBeforeClose() const { - return static_cast<unsigned int>(timeBeforeClose_.total_milliseconds()); + return static_cast<uint64_t>(timeBeforeClose_.total_milliseconds()); } - void SetMillisecondsBeforeClose(unsigned int ms); + void SetMillisecondsBeforeClose(uint64_t ms); const std::string& GetLocalApplicationEntityTitle() const;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ExportedResource.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,69 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "ExportedResource.h" + +#include "../Core/OrthancException.h" + +namespace Orthanc +{ + void ExportedResource::Format(Json::Value& item) const + { + item = Json::objectValue; + item["Seq"] = static_cast<int>(seq_); + item["ResourceType"] = EnumerationToString(resourceType_); + item["ID"] = publicId_; + item["Path"] = GetBasePath(resourceType_, publicId_); + item["RemoteModality"] = modality_; + item["Date"] = date_; + + // WARNING: Do not add "break" below and do not reorder the case items! + switch (resourceType_) + { + case ResourceType_Instance: + item["SOPInstanceUID"] = sopInstanceUid_; + + case ResourceType_Series: + item["SeriesInstanceUID"] = seriesInstanceUid_; + + case ResourceType_Study: + item["StudyInstanceUID"] = studyInstanceUid_; + + case ResourceType_Patient: + item["PatientID"] = patientId_; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ExportedResource.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,125 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "ServerEnumerations.h" +#include "../Core/Toolbox.h" + +#include <string> +#include <json/value.h> + +namespace Orthanc +{ + class ExportedResource + { + private: + int64_t seq_; + ResourceType resourceType_; + std::string publicId_; + std::string modality_; + std::string date_; + std::string patientId_; + std::string studyInstanceUid_; + std::string seriesInstanceUid_; + std::string sopInstanceUid_; + + public: + ExportedResource(int64_t seq, + ResourceType resourceType, + const std::string& publicId, + const std::string& modality, + const std::string& date, + const std::string& patientId, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + const std::string& sopInstanceUid) : + seq_(seq), + resourceType_(resourceType), + publicId_(publicId), + modality_(modality), + date_(date), + patientId_(patientId), + studyInstanceUid_(studyInstanceUid), + seriesInstanceUid_(seriesInstanceUid), + sopInstanceUid_(sopInstanceUid) + { + } + + int64_t GetSeq() const + { + return seq_; + } + + ResourceType GetResourceType() const + { + return resourceType_; + } + + const std::string& GetPublicId() const + { + return publicId_; + } + + const std::string& GetModality() const + { + return modality_; + } + + const std::string& GetDate() const + { + return date_; + } + + const std::string& GetPatientId() const + { + return patientId_; + } + + const std::string& GetStudyInstanceUid() const + { + return studyInstanceUid_; + } + + const std::string& GetSeriesInstanceUid() const + { + return seriesInstanceUid_; + } + + const std::string& GetSopInstanceUid() const + { + return sopInstanceUid_; + } + + void Format(Json::Value& item) const; + }; +}
--- a/OrthancServer/FromDcmtkBridge.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/FromDcmtkBridge.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -41,6 +41,7 @@ #include "FromDcmtkBridge.h" #include "ToDcmtkBridge.h" +#include "OrthancInitialization.h" #include "../Core/Toolbox.h" #include "../Core/OrthancException.h" #include "../Core/ImageFormats/PngWriter.h" @@ -61,6 +62,7 @@ #include <dcmtk/dcmdata/dcistrmb.h> #include <dcmtk/dcmdata/dcuid.h> #include <dcmtk/dcmdata/dcmetinf.h> +#include <dcmtk/dcmdata/dcdeftag.h> #include <dcmtk/dcmdata/dcvrae.h> #include <dcmtk/dcmdata/dcvras.h> @@ -86,6 +88,7 @@ #include <dcmtk/dcmdata/dcpixel.h> #include <dcmtk/dcmdata/dcpixseq.h> #include <dcmtk/dcmdata/dcpxitem.h> +#include <dcmtk/dcmdata/dcvrat.h> #include <boost/math/special_functions/round.hpp> @@ -115,8 +118,46 @@ GetCharValue(c[3])); } + + Encoding FromDcmtkBridge::DetectEncoding(DcmDataset& dataset) + { + // By default, Latin1 encoding is assumed + std::string s = Configuration::GetGlobalStringParameter("DefaultEncoding", ""); + Encoding encoding = s.empty() ? Encoding_Latin1 : StringToEncoding(s.c_str()); + + OFString tmp; + if (dataset.findAndGetOFString(DCM_SpecificCharacterSet, tmp).good()) + { + std::string characterSet = Toolbox::StripSpaces(std::string(tmp.c_str())); + + if (characterSet.empty()) + { + // Empty specific character set tag: Use the default encoding + } + else if (GetDicomEncoding(encoding, characterSet.c_str())) + { + // The specific character set is supported by the Orthanc core + } + else + { + LOG(WARNING) << "Value of Specific Character Set (0008,0005) is not supported: " << characterSet + << ", fallback to ASCII (remove all special characters)"; + encoding = Encoding_Ascii; + } + } + else + { + // No specific character set tag: Use the default encoding + } + + return encoding; + } + + void FromDcmtkBridge::Convert(DicomMap& target, DcmDataset& dataset) { + Encoding encoding = DetectEncoding(dataset); + target.Clear(); for (unsigned long i = 0; i < dataset.card(); i++) { @@ -125,19 +166,52 @@ { target.SetValue(element->getTag().getGTag(), element->getTag().getETag(), - ConvertLeafElement(*element)); + ConvertLeafElement(*element, encoding)); } } } + DicomTag FromDcmtkBridge::Convert(const DcmTag& tag) + { + return DicomTag(tag.getGTag(), tag.getETag()); + } + + DicomTag FromDcmtkBridge::GetTag(const DcmElement& element) { return DicomTag(element.getGTag(), element.getETag()); } - DicomValue* FromDcmtkBridge::ConvertLeafElement(DcmElement& element) + bool FromDcmtkBridge::IsPrivateTag(DcmTag& tag) + { +#if 1 + DcmTagKey tmp(tag.getGTag(), tag.getETag()); + return tmp.isPrivate(); +#else + // Implementation for Orthanc versions <= 0.8.5 + return (tag.getPrivateCreator() != NULL || + !strcmp("PrivateCreator", tag.getTagName())); // TODO - This may change with future versions of DCMTK +#endif + } + + + bool FromDcmtkBridge::IsPrivateTag(const DicomTag& tag) + { +#if 1 + DcmTagKey tmp(tag.GetGroup(), tag.GetElement()); + return tmp.isPrivate(); +#else + // Implementation for Orthanc versions <= 0.8.5 + DcmTag tmp(tag.GetGroup(), tag.GetElement()); + return IsPrivateTag(tmp); +#endif + } + + + DicomValue* FromDcmtkBridge::ConvertLeafElement(DcmElement& element, + Encoding encoding) { if (!element.isLeaf()) { @@ -151,7 +225,7 @@ c != NULL) { std::string s(c); - std::string utf8 = Toolbox::ConvertToUtf8(s, Encoding_Latin1); // TODO Parameter? + std::string utf8 = Toolbox::ConvertToUtf8(s, encoding); return new DicomString(utf8); } else @@ -173,9 +247,8 @@ case EVR_OB: // other byte case EVR_OF: // other float case EVR_OW: // other word - case EVR_AT: // attribute tag case EVR_UN: // unknown value representation - return new DicomNullValue(); + return new DicomNullValue; /** * String types, should never happen at this point because of @@ -197,11 +270,11 @@ case EVR_UT: // unlimited text case EVR_PN: // person name case EVR_UI: // unique identifier - return new DicomNullValue(); + return new DicomNullValue; /** - * Numerical types + * Numberic types **/ case EVR_SL: // signed long @@ -210,7 +283,7 @@ if (dynamic_cast<DcmSignedLong&>(element).getSint32(f).good()) return new DicomString(boost::lexical_cast<std::string>(f)); else - return new DicomNullValue(); + return new DicomNullValue; } case EVR_SS: // signed short @@ -219,7 +292,7 @@ if (dynamic_cast<DcmSignedShort&>(element).getSint16(f).good()) return new DicomString(boost::lexical_cast<std::string>(f)); else - return new DicomNullValue(); + return new DicomNullValue; } case EVR_UL: // unsigned long @@ -228,7 +301,7 @@ if (dynamic_cast<DcmUnsignedLong&>(element).getUint32(f).good()) return new DicomString(boost::lexical_cast<std::string>(f)); else - return new DicomNullValue(); + return new DicomNullValue; } case EVR_US: // unsigned short @@ -237,7 +310,7 @@ if (dynamic_cast<DcmUnsignedShort&>(element).getUint16(f).good()) return new DicomString(boost::lexical_cast<std::string>(f)); else - return new DicomNullValue(); + return new DicomNullValue; } case EVR_FL: // float single-precision @@ -246,7 +319,7 @@ if (dynamic_cast<DcmFloatingPointSingle&>(element).getFloat32(f).good()) return new DicomString(boost::lexical_cast<std::string>(f)); else - return new DicomNullValue(); + return new DicomNullValue; } case EVR_FD: // float double-precision @@ -255,7 +328,26 @@ if (dynamic_cast<DcmFloatingPointDouble&>(element).getFloat64(f).good()) return new DicomString(boost::lexical_cast<std::string>(f)); else - return new DicomNullValue(); + return new DicomNullValue; + } + + + /** + * Attribute tag. + **/ + + case EVR_AT: + { + DcmTagKey tag; + if (dynamic_cast<DcmAttributeTag&>(element).getTagVal(tag, 0).good()) + { + DicomTag t(tag.getGroup(), tag.getElement()); + return new DicomString(t.Format()); + } + else + { + return new DicomNullValue; + } } @@ -313,25 +405,28 @@ static void StoreElement(Json::Value& target, DcmElement& element, - unsigned int maxStringLength); + unsigned int maxStringLength, + Encoding encoding); static void StoreItem(Json::Value& target, DcmItem& item, - unsigned int maxStringLength) + unsigned int maxStringLength, + Encoding encoding) { target = Json::Value(Json::objectValue); for (unsigned long i = 0; i < item.card(); i++) { DcmElement* element = item.getElement(i); - StoreElement(target, *element, maxStringLength); + StoreElement(target, *element, maxStringLength, encoding); } } static void StoreElement(Json::Value& target, DcmElement& element, - unsigned int maxStringLength) + unsigned int maxStringLength, + Encoding encoding) { assert(target.type() == Json::objectValue); @@ -356,7 +451,7 @@ value["PrivateCreator"] = tagbis.getPrivateCreator(); } - std::auto_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement(element)); + std::auto_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement(element, encoding)); if (v->IsNull()) { value["Type"] = "Null"; @@ -393,7 +488,7 @@ { DcmItem* child = sequence.getItem(i); Json::Value& v = children.append(Json::objectValue); - StoreItem(v, *child, maxStringLength); + StoreItem(v, *child, maxStringLength, encoding); } target[formattedTag]["Name"] = tagName; @@ -407,7 +502,7 @@ DcmDataset& dataset, unsigned int maxStringLength) { - StoreItem(root, dataset, maxStringLength); + StoreItem(root, dataset, maxStringLength, DetectEncoding(dataset)); } @@ -475,7 +570,7 @@ isxdigit(name[1]) && isxdigit(name[2]) && isxdigit(name[3]) && - name[4] == '-' && + (name[4] == '-' || name[4] == ',') && isxdigit(name[5]) && isxdigit(name[6]) && isxdigit(name[7]) && @@ -573,7 +668,7 @@ } bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer, - DcmDataset* dataSet) + DcmDataset& dataSet) { // Determine the transfer syntax which shall be used to write the // information to the file. We always switch to the Little Endian @@ -588,7 +683,7 @@ * dataset into memory. We now keep the original transfer syntax * (if available). **/ - E_TransferSyntax xfer = dataSet->getOriginalXfer(); + E_TransferSyntax xfer = dataSet.getOriginalXfer(); if (xfer == EXS_Unknown) { // No information about the original transfer syntax: This is @@ -599,8 +694,9 @@ E_EncodingType encodingType = /*opt_sequenceType*/ EET_ExplicitLength; // Create the meta-header information - DcmFileFormat ff(dataSet); + DcmFileFormat ff(&dataSet); ff.validateMetaInfo(xfer); + ff.removeInvalidGroups(); // Create a memory buffer with the proper size uint32_t s = ff.calcElementLength(xfer, encodingType); @@ -625,4 +721,12 @@ return false; } } + + + bool FromDcmtkBridge::IsPNValueRepresentation(const DicomTag& tag) + { + DcmTag t(tag.GetGroup(), tag.GetElement()); + return t.getEVR() == EVR_PN; + } + }
--- a/OrthancServer/FromDcmtkBridge.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/FromDcmtkBridge.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -44,11 +44,20 @@ class FromDcmtkBridge { public: + static Encoding DetectEncoding(DcmDataset& dataset); + static void Convert(DicomMap& target, DcmDataset& dataset); + static DicomTag Convert(const DcmTag& tag); + static DicomTag GetTag(const DcmElement& element); - static DicomValue* ConvertLeafElement(DcmElement& element); + static bool IsPrivateTag(DcmTag& tag); + + static bool IsPrivateTag(const DicomTag& tag); + + static DicomValue* ConvertLeafElement(DcmElement& element, + Encoding encoding); static void ToJson(Json::Value& target, DcmDataset& dataset, @@ -95,6 +104,8 @@ static std::string GenerateUniqueIdentifier(ResourceType level); static bool SaveToMemoryBuffer(std::string& buffer, - DcmDataset* dataSet); + DcmDataset& dataSet); + + static bool IsPNValueRepresentation(const DicomTag& tag); }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/IDatabaseWrapper.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,181 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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/DicomFormat/DicomMap.h" +#include "../Core/SQLite/ITransaction.h" +#include "../Core/FileStorage/FileInfo.h" +#include "IServerIndexListener.h" +#include "ExportedResource.h" + +#include <list> +#include <boost/noncopyable.hpp> + +namespace Orthanc +{ + class IDatabaseWrapper : public boost::noncopyable + { + public: + virtual ~IDatabaseWrapper() + { + } + + virtual void AddAttachment(int64_t id, + const FileInfo& attachment) = 0; + + virtual void AttachChild(int64_t parent, + int64_t child) = 0; + + virtual void ClearChanges() = 0; + + virtual void ClearExportedResources() = 0; + + virtual int64_t CreateResource(const std::string& publicId, + ResourceType type) = 0; + + virtual void DeleteAttachment(int64_t id, + FileContentType attachment) = 0; + + virtual void DeleteMetadata(int64_t id, + MetadataType type) = 0; + + virtual void DeleteResource(int64_t id) = 0; + + virtual void FlushToDisk() = 0; + + virtual bool HasFlushToDisk() const = 0; + + virtual void GetAllMetadata(std::map<MetadataType, std::string>& target, + int64_t id) = 0; + + virtual void GetAllPublicIds(std::list<std::string>& target, + ResourceType resourceType) = 0; + + + virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/, + bool& done /*out*/, + int64_t since, + uint32_t maxResults) = 0; + + virtual void GetChildrenInternalId(std::list<int64_t>& target, + int64_t id) = 0; + + virtual void GetChildrenPublicId(std::list<std::string>& target, + int64_t id) = 0; + + virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/, + bool& done /*out*/, + int64_t since, + uint32_t maxResults) = 0; + + virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/) = 0; + + virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/) = 0; + + virtual void GetMainDicomTags(DicomMap& map, + int64_t id) = 0; + + virtual std::string GetPublicId(int64_t resourceId) = 0; + + virtual uint64_t GetResourceCount(ResourceType resourceType) = 0; + + virtual ResourceType GetResourceType(int64_t resourceId) = 0; + + virtual uint64_t GetTotalCompressedSize() = 0; + + virtual uint64_t GetTotalUncompressedSize() = 0; + + virtual bool IsExistingResource(int64_t internalId) = 0; + + virtual bool IsProtectedPatient(int64_t internalId) = 0; + + virtual void ListAvailableMetadata(std::list<MetadataType>& target, + int64_t id) = 0; + + virtual void ListAvailableAttachments(std::list<FileContentType>& target, + int64_t id) = 0; + + virtual void LogChange(int64_t internalId, + const ServerIndexChange& change) = 0; + + virtual void LogExportedResource(const ExportedResource& resource) = 0; + + virtual bool LookupAttachment(FileInfo& attachment, + int64_t id, + FileContentType contentType) = 0; + + virtual bool LookupGlobalProperty(std::string& target, + GlobalProperty property) = 0; + + virtual void LookupIdentifier(std::list<int64_t>& target, + const DicomTag& tag, + const std::string& value) = 0; + + virtual void LookupIdentifier(std::list<int64_t>& target, + const std::string& value) = 0; + + virtual bool LookupMetadata(std::string& target, + int64_t id, + MetadataType type) = 0; + + virtual bool LookupParent(int64_t& parentId, + int64_t resourceId) = 0; + + virtual bool LookupResource(int64_t& id, + ResourceType& type, + const std::string& publicId) = 0; + + virtual bool SelectPatientToRecycle(int64_t& internalId) = 0; + + virtual bool SelectPatientToRecycle(int64_t& internalId, + int64_t patientIdToAvoid) = 0; + + virtual void SetGlobalProperty(GlobalProperty property, + const std::string& value) = 0; + + virtual void SetMainDicomTag(int64_t id, + const DicomTag& tag, + const std::string& value) = 0; + + virtual void SetMetadata(int64_t id, + MetadataType type, + const std::string& value) = 0; + + virtual void SetProtectedPatient(int64_t internalId, + bool isProtected) = 0; + + virtual SQLite::ITransaction* StartTransaction() = 0; + + virtual void SetListener(IServerIndexListener& listener) = 0; + }; +}
--- a/OrthancServer/IServerIndexListener.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/IServerIndexListener.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -34,6 +34,7 @@ #include <string> #include "ServerEnumerations.h" +#include "ServerIndexChange.h" namespace Orthanc { @@ -48,5 +49,7 @@ const std::string& publicId) = 0; virtual void SignalFileDeleted(const FileInfo& info) = 0; + + virtual void SignalChange(const ServerIndexChange& change) = 0; }; }
--- a/OrthancServer/Internals/CommandDispatcher.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/Internals/CommandDispatcher.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -234,10 +234,156 @@ #endif + namespace Orthanc { namespace Internals { + /** + * EXTRACT OF FILE "dcmdata/libsrc/dcuid.cc" FROM DCMTK 3.6.0 + * (dcmAllStorageSOPClassUIDs). + * + * an array of const strings containing all known Storage SOP + * Classes that fit into the conventional + * PATIENT-STUDY-SERIES-INSTANCE information model, + * i.e. everything a Storage SCP might want to store in a PACS. + * Special cases such as hanging protocol storage or the Storage + * SOP Class are not included in this list. + * + * THIS LIST CONTAINS ALL STORAGE SOP CLASSES INCLUDING RETIRED + * ONES AND IS LARGER THAN 64 ENTRIES. + */ + + const char* orthancStorageSOPClassUIDs[] = + { + UID_AmbulatoryECGWaveformStorage, + UID_ArterialPulseWaveformStorage, + UID_AutorefractionMeasurementsStorage, + UID_BasicStructuredDisplayStorage, + UID_BasicTextSRStorage, + UID_BasicVoiceAudioWaveformStorage, + UID_BlendingSoftcopyPresentationStateStorage, + UID_BreastTomosynthesisImageStorage, + UID_CardiacElectrophysiologyWaveformStorage, + UID_ChestCADSRStorage, + UID_ColonCADSRStorage, + UID_ColorSoftcopyPresentationStateStorage, + UID_ComprehensiveSRStorage, + UID_ComputedRadiographyImageStorage, + UID_CTImageStorage, + UID_DeformableSpatialRegistrationStorage, + UID_DigitalIntraOralXRayImageStorageForPresentation, + UID_DigitalIntraOralXRayImageStorageForProcessing, + UID_DigitalMammographyXRayImageStorageForPresentation, + UID_DigitalMammographyXRayImageStorageForProcessing, + UID_DigitalXRayImageStorageForPresentation, + UID_DigitalXRayImageStorageForProcessing, + UID_EncapsulatedCDAStorage, + UID_EncapsulatedPDFStorage, + UID_EnhancedCTImageStorage, + UID_EnhancedMRColorImageStorage, + UID_EnhancedMRImageStorage, + UID_EnhancedPETImageStorage, + UID_EnhancedSRStorage, + UID_EnhancedUSVolumeStorage, + UID_EnhancedXAImageStorage, + UID_EnhancedXRFImageStorage, + UID_GeneralAudioWaveformStorage, + UID_GeneralECGWaveformStorage, + UID_GenericImplantTemplateStorage, + UID_GrayscaleSoftcopyPresentationStateStorage, + UID_HemodynamicWaveformStorage, + UID_ImplantAssemblyTemplateStorage, + UID_ImplantationPlanSRDocumentStorage, + UID_ImplantTemplateGroupStorage, + UID_IntraocularLensCalculationsStorage, + UID_KeratometryMeasurementsStorage, + UID_KeyObjectSelectionDocumentStorage, + UID_LensometryMeasurementsStorage, + UID_MacularGridThicknessAndVolumeReportStorage, + UID_MammographyCADSRStorage, + UID_MRImageStorage, + UID_MRSpectroscopyStorage, + UID_MultiframeGrayscaleByteSecondaryCaptureImageStorage, + UID_MultiframeGrayscaleWordSecondaryCaptureImageStorage, + UID_MultiframeSingleBitSecondaryCaptureImageStorage, + UID_MultiframeTrueColorSecondaryCaptureImageStorage, + UID_NuclearMedicineImageStorage, + UID_OphthalmicAxialMeasurementsStorage, + UID_OphthalmicPhotography16BitImageStorage, + UID_OphthalmicPhotography8BitImageStorage, + UID_OphthalmicTomographyImageStorage, + UID_OphthalmicVisualFieldStaticPerimetryMeasurementsStorage, + UID_PositronEmissionTomographyImageStorage, + UID_ProcedureLogStorage, + UID_PseudoColorSoftcopyPresentationStateStorage, + UID_RawDataStorage, + UID_RealWorldValueMappingStorage, + UID_RespiratoryWaveformStorage, + UID_RTBeamsTreatmentRecordStorage, + UID_RTBrachyTreatmentRecordStorage, + UID_RTDoseStorage, + UID_RTImageStorage, + UID_RTIonBeamsTreatmentRecordStorage, + UID_RTIonPlanStorage, + UID_RTPlanStorage, + UID_RTStructureSetStorage, + UID_RTTreatmentSummaryRecordStorage, + UID_SecondaryCaptureImageStorage, + UID_SegmentationStorage, + UID_SpatialFiducialsStorage, + UID_SpatialRegistrationStorage, + UID_SpectaclePrescriptionReportStorage, + UID_StereometricRelationshipStorage, + UID_SubjectiveRefractionMeasurementsStorage, + UID_SurfaceSegmentationStorage, + UID_TwelveLeadECGWaveformStorage, + UID_UltrasoundImageStorage, + UID_UltrasoundMultiframeImageStorage, + UID_VideoEndoscopicImageStorage, + UID_VideoMicroscopicImageStorage, + UID_VideoPhotographicImageStorage, + UID_VisualAcuityMeasurementsStorage, + UID_VLEndoscopicImageStorage, + UID_VLMicroscopicImageStorage, + UID_VLPhotographicImageStorage, + UID_VLSlideCoordinatesMicroscopicImageStorage, + UID_VLWholeSlideMicroscopyImageStorage, + UID_XAXRFGrayscaleSoftcopyPresentationStateStorage, + UID_XRay3DAngiographicImageStorage, + UID_XRay3DCraniofacialImageStorage, + UID_XRayAngiographicImageStorage, + UID_XRayRadiationDoseSRStorage, + UID_XRayRadiofluoroscopicImageStorage, + // retired + UID_RETIRED_HardcopyColorImageStorage, + UID_RETIRED_HardcopyGrayscaleImageStorage, + UID_RETIRED_NuclearMedicineImageStorage, + UID_RETIRED_StandaloneCurveStorage, + UID_RETIRED_StandaloneModalityLUTStorage, + UID_RETIRED_StandaloneOverlayStorage, + UID_RETIRED_StandalonePETCurveStorage, + UID_RETIRED_StandaloneVOILUTStorage, + UID_RETIRED_StoredPrintStorage, + UID_RETIRED_UltrasoundImageStorage, + UID_RETIRED_UltrasoundMultiframeImageStorage, + UID_RETIRED_VLImageStorage, + UID_RETIRED_VLMultiFrameImageStorage, + UID_RETIRED_XRayAngiographicBiPlaneImageStorage, + // draft + UID_DRAFT_SRAudioStorage, + UID_DRAFT_SRComprehensiveStorage, + UID_DRAFT_SRDetailStorage, + UID_DRAFT_SRTextStorage, + UID_DRAFT_WaveformStorage, + UID_DRAFT_RTBeamsDeliveryInstructionStorage, + NULL + }; + + const int orthancStorageSOPClassUIDsCount = (sizeof(orthancStorageSOPClassUIDs) / sizeof(const char*)) - 1; + + + OFCondition AssociationCleanup(T_ASC_Association *assoc) { OFString temp_str; @@ -313,7 +459,38 @@ return NULL; } - LOG(INFO) << "Association Received"; + // Retrieve the AET and the IP address of the remote modality + std::string callingAet; + std::string callingIp; + std::string calledAet; + + { + DIC_AE callingAet_C; + DIC_AE calledAet_C; + DIC_AE callingIp_C; + DIC_AE calledIP_C; + if (ASC_getAPTitles(assoc->params, callingAet_C, calledAet_C, NULL).bad() || + ASC_getPresentationAddresses(assoc->params, callingIp_C, calledIP_C).bad()) + { + T_ASC_RejectParameters rej = + { + ASC_RESULT_REJECTEDPERMANENT, + ASC_SOURCE_SERVICEUSER, + ASC_REASON_SU_NOREASON + }; + ASC_rejectAssociation(assoc, &rej); + AssociationCleanup(assoc); + return NULL; + } + + callingIp = std::string(/*OFSTRING_GUARD*/(callingIp_C)); + callingAet = std::string(/*OFSTRING_GUARD*/(callingAet_C)); + calledAet = (/*OFSTRING_GUARD*/(calledAet_C)); + } + + LOG(INFO) << "Association Received from AET " << callingAet + << " on IP " << callingIp; + std::vector<const char*> transferSyntaxes; @@ -323,39 +500,75 @@ transferSyntaxes.push_back(UID_LittleEndianImplicitTransferSyntax); // New transfer syntaxes supported since Orthanc 0.7.2 - transferSyntaxes.push_back(UID_DeflatedExplicitVRLittleEndianTransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess1TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess2_4TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess3_5TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess6_8TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess7_9TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess10_12TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess11_13TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess14TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess15TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess16_18TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess17_19TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess20_22TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess21_23TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess24_26TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess25_27TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess28TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess29TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess14SV1TransferSyntax); - transferSyntaxes.push_back(UID_JPEGLSLosslessTransferSyntax); - transferSyntaxes.push_back(UID_JPEGLSLossyTransferSyntax); - transferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax); - transferSyntaxes.push_back(UID_JPEG2000TransferSyntax); - transferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionLosslessOnlyTransferSyntax); - transferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionTransferSyntax); - transferSyntaxes.push_back(UID_JPIPReferencedTransferSyntax); - transferSyntaxes.push_back(UID_JPIPReferencedDeflateTransferSyntax); - transferSyntaxes.push_back(UID_MPEG2MainProfileAtMainLevelTransferSyntax); - transferSyntaxes.push_back(UID_MPEG2MainProfileAtHighLevelTransferSyntax); - transferSyntaxes.push_back(UID_RLELosslessTransferSyntax); + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Deflated)) + { + transferSyntaxes.push_back(UID_DeflatedExplicitVRLittleEndianTransferSyntax); + } + + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Jpeg)) + { + transferSyntaxes.push_back(UID_JPEGProcess1TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess2_4TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess3_5TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess6_8TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess7_9TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess10_12TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess11_13TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess14TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess15TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess16_18TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess17_19TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess20_22TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess21_23TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess24_26TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess25_27TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess28TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess29TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess14SV1TransferSyntax); + transferSyntaxes.push_back(UID_JPEGLSLosslessTransferSyntax); + transferSyntaxes.push_back(UID_JPEGLSLossyTransferSyntax); + } + + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Jpeg2000)) + { + transferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax); + transferSyntaxes.push_back(UID_JPEG2000TransferSyntax); + } + + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_JpegLossless)) + { + transferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax); + transferSyntaxes.push_back(UID_JPEG2000TransferSyntax); + transferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionLosslessOnlyTransferSyntax); + transferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionTransferSyntax); + } + + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Jpip)) + { + transferSyntaxes.push_back(UID_JPIPReferencedTransferSyntax); + transferSyntaxes.push_back(UID_JPIPReferencedDeflateTransferSyntax); + } + + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Mpeg2)) + { + transferSyntaxes.push_back(UID_MPEG2MainProfileAtMainLevelTransferSyntax); + transferSyntaxes.push_back(UID_MPEG2MainProfileAtHighLevelTransferSyntax); + } + + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Rle)) + { + transferSyntaxes.push_back(UID_RLELosslessTransferSyntax); + } /* accept the Verification SOP Class if presented */ - cond = ASC_acceptContextsWithPreferredTransferSyntaxes( assoc->params, &knownAbstractSyntaxes[0], knownAbstractSyntaxes.size(), &transferSyntaxes[0], transferSyntaxes.size()); + cond = ASC_acceptContextsWithPreferredTransferSyntaxes(assoc->params, &knownAbstractSyntaxes[0], knownAbstractSyntaxes.size(), &transferSyntaxes[0], transferSyntaxes.size()); if (cond.bad()) { LOG(INFO) << cond.text(); @@ -364,7 +577,7 @@ } /* the array of Storage SOP Class UIDs comes from dcuid.h */ - cond = ASC_acceptContextsWithPreferredTransferSyntaxes( assoc->params, dcmAllStorageSOPClassUIDs, numberOfAllDcmStorageSOPClassUIDs, &transferSyntaxes[0], transferSyntaxes.size()); + cond = ASC_acceptContextsWithPreferredTransferSyntaxes(assoc->params, orthancStorageSOPClassUIDs, orthancStorageSOPClassUIDsCount, &transferSyntaxes[0], transferSyntaxes.size()); if (cond.bad()) { LOG(INFO) << cond.text(); @@ -409,62 +622,38 @@ return NULL; } - std::string callingIP; - std::string callingTitle; - /* check the AETs */ + if (!server.IsMyAETitle(calledAet)) { - DIC_AE callingTitle_C; - DIC_AE calledTitle_C; - DIC_AE callingIP_C; - DIC_AE calledIP_C; - if (ASC_getAPTitles(assoc->params, callingTitle_C, calledTitle_C, NULL).bad() || - ASC_getPresentationAddresses(assoc->params, callingIP_C, calledIP_C).bad()) - { - T_ASC_RejectParameters rej = - { - ASC_RESULT_REJECTEDPERMANENT, - ASC_SOURCE_SERVICEUSER, - ASC_REASON_SU_NOREASON - }; - ASC_rejectAssociation(assoc, &rej); - AssociationCleanup(assoc); - return NULL; - } - - callingIP = std::string(/*OFSTRING_GUARD*/(callingIP_C)); - callingTitle = std::string(/*OFSTRING_GUARD*/(callingTitle_C)); - std::string calledTitle(/*OFSTRING_GUARD*/(calledTitle_C)); - - if (!server.IsMyAETitle(calledTitle)) - { - T_ASC_RejectParameters rej = - { - ASC_RESULT_REJECTEDPERMANENT, - ASC_SOURCE_SERVICEUSER, - ASC_REASON_SU_CALLEDAETITLENOTRECOGNIZED - }; - ASC_rejectAssociation(assoc, &rej); - AssociationCleanup(assoc); - return NULL; - } - - if (server.HasApplicationEntityFilter() && - !server.GetApplicationEntityFilter().IsAllowedConnection(callingIP, callingTitle)) - { - T_ASC_RejectParameters rej = - { - ASC_RESULT_REJECTEDPERMANENT, - ASC_SOURCE_SERVICEUSER, - ASC_REASON_SU_CALLINGAETITLENOTRECOGNIZED - }; - ASC_rejectAssociation(assoc, &rej); - AssociationCleanup(assoc); - return NULL; - } + LOG(WARNING) << "Rejected association, because of a bad called AET in the request (" << calledAet << ")"; + T_ASC_RejectParameters rej = + { + ASC_RESULT_REJECTEDPERMANENT, + ASC_SOURCE_SERVICEUSER, + ASC_REASON_SU_CALLEDAETITLENOTRECOGNIZED + }; + ASC_rejectAssociation(assoc, &rej); + AssociationCleanup(assoc); + return NULL; } - if (opt_rejectWithoutImplementationUID && strlen(assoc->params->theirImplementationClassUID) == 0) + if (server.HasApplicationEntityFilter() && + !server.GetApplicationEntityFilter().IsAllowedConnection(callingIp, callingAet)) + { + LOG(WARNING) << "Rejected association for remote AET " << callingAet << " on IP " << callingIp; + T_ASC_RejectParameters rej = + { + ASC_RESULT_REJECTEDPERMANENT, + ASC_SOURCE_SERVICEUSER, + ASC_REASON_SU_CALLINGAETITLENOTRECOGNIZED + }; + ASC_rejectAssociation(assoc, &rej); + AssociationCleanup(assoc); + return NULL; + } + + if (opt_rejectWithoutImplementationUID && + strlen(assoc->params->theirImplementationClassUID) == 0) { /* reject: the no implementation Class UID provided */ T_ASC_RejectParameters rej = @@ -498,7 +687,7 @@ } IApplicationEntityFilter* filter = server.HasApplicationEntityFilter() ? &server.GetApplicationEntityFilter() : NULL; - return new CommandDispatcher(server, assoc, callingIP, callingTitle, filter); + return new CommandDispatcher(server, assoc, callingIp, callingAet, filter); } bool CommandDispatcher::Step() @@ -580,14 +769,16 @@ // Check whether this request is allowed by the security filter if (supported && + request != DicomRequestType_Echo && // Always allow incoming ECHO requests filter_ != NULL && !filter_->IsAllowedRequest(callingIP_, callingAETitle_, request)) { LOG(ERROR) << EnumerationToString(request) << " requests are disallowed for the AET \"" << callingAETitle_ << "\""; - cond = DIMSE_BADCOMMANDTYPE; + cond = DIMSE_ILLEGALASSOCIATION; supported = false; + finished = true; } // in case we received a supported message, process this command @@ -668,7 +859,7 @@ } - OFCondition EchoScp( T_ASC_Association * assoc, T_DIMSE_Message * msg, T_ASC_PresentationContextID presID) + OFCondition EchoScp(T_ASC_Association * assoc, T_DIMSE_Message * msg, T_ASC_PresentationContextID presID) { OFString temp_str; LOG(INFO) << "Received Echo Request";
--- a/OrthancServer/Internals/CommandDispatcher.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/Internals/CommandDispatcher.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancServer/Internals/DicomImageDecoder.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/Internals/DicomImageDecoder.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -225,7 +225,6 @@ private: std::string psmct_; std::auto_ptr<DicomIntegerPixelAccessor> slowAccessor_; - std::auto_ptr<ImageAccessor> fastAccessor_; public: void Setup(DcmDataset& dataset, @@ -233,7 +232,6 @@ { psmct_.clear(); slowAccessor_.reset(NULL); - fastAccessor_.reset(NULL); // See also: http://support.dcmtk.org/wiki/dcmtk/howto/accessing-compressed-data @@ -272,13 +270,6 @@ } slowAccessor_->SetCurrentFrame(frame); - - - /** - * If possible, create a fast ImageAccessor to the image buffer. - **/ - - } unsigned int GetWidth() const @@ -305,15 +296,10 @@ return *slowAccessor_; } - bool HasFastAccessor() const + unsigned int GetSize() const { - return fastAccessor_.get() != NULL; - } - - const ImageAccessor& GetFastAccessor() const - { - assert(HasFastAccessor()); - return *fastAccessor_; + assert(slowAccessor_.get() != NULL); + return slowAccessor_->GetSize(); } }; @@ -332,7 +318,9 @@ LOG(WARNING) << "Unsupported DICOM image: " << info.GetBitsStored() << "bpp, " << info.GetChannelCount() << " channels, " << (info.IsSigned() ? "signed" : "unsigned") - << (info.IsPlanar() ? ", planar" : ", non-planar"); + << (info.IsPlanar() ? ", planar, " : ", non-planar, ") + << EnumerationToString(info.GetPhotometricInterpretation()) + << " photometric interpretation"; throw OrthancException(ErrorCode_NotImplemented); } @@ -435,16 +423,22 @@ { try { - ImageAccessor sourceImage; - sourceImage.AssignReadOnly(sourceFormat, - info.GetWidth(), - info.GetHeight(), - info.GetWidth() * GetBytesPerPixel(sourceFormat), - source.GetAccessor().GetPixelData()); + size_t frameSize = info.GetHeight() * info.GetWidth() * GetBytesPerPixel(sourceFormat); + if ((frame + 1) * frameSize <= source.GetSize()) + { + const uint8_t* buffer = reinterpret_cast<const uint8_t*>(source.GetAccessor().GetPixelData()); - ImageProcessing::Convert(targetAccessor, sourceImage); - ImageProcessing::ShiftRight(targetAccessor, info.GetShift()); - fastVersionSuccess = true; + ImageAccessor sourceImage; + sourceImage.AssignReadOnly(sourceFormat, + info.GetWidth(), + info.GetHeight(), + info.GetWidth() * GetBytesPerPixel(sourceFormat), + buffer + frame * frameSize); + + ImageProcessing::Convert(targetAccessor, sourceImage); + ImageProcessing::ShiftRight(targetAccessor, info.GetShift()); + fastVersionSuccess = true; + } } catch (OrthancException&) { @@ -452,7 +446,6 @@ } } - /** * Slow version : loop over the DICOM buffer, storing its value * into the target image. @@ -589,10 +582,18 @@ } + static bool IsColorImage(PixelFormat format) + { + return (format == PixelFormat_RGB24 || + format == PixelFormat_RGBA32); + } + + bool DicomImageDecoder::DecodeAndTruncate(ImageBuffer& target, DcmDataset& dataset, unsigned int frame, - PixelFormat format) + PixelFormat format, + bool allowColorConversion) { // TODO Special case for uncompressed images @@ -602,6 +603,19 @@ return false; } + // If specified, prevent the conversion between color and + // grayscale images + bool isSourceColor = IsColorImage(source.GetFormat()); + bool isTargetColor = IsColorImage(format); + + if (!allowColorConversion) + { + if (isSourceColor ^ isTargetColor) + { + return false; + } + } + if (source.GetFormat() == format) { // No conversion is required, return the temporary image
--- a/OrthancServer/Internals/DicomImageDecoder.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/Internals/DicomImageDecoder.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -72,7 +72,8 @@ static bool DecodeAndTruncate(ImageBuffer& target, DcmDataset& dataset, unsigned int frame, - PixelFormat format); + PixelFormat format, + bool allowColorConversion); static bool DecodePreview(ImageBuffer& target, DcmDataset& dataset,
--- a/OrthancServer/Internals/FindScp.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/Internals/FindScp.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancServer/Internals/FindScp.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/Internals/FindScp.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancServer/Internals/MoveScp.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/Internals/MoveScp.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancServer/Internals/MoveScp.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/Internals/MoveScp.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancServer/Internals/StoreScp.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/Internals/StoreScp.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -102,7 +102,8 @@ struct StoreCallbackData { IStoreRequestHandler* handler; - const char* distantAET; + const char* remoteAET; + const char* calledAET; const char* modality; const char* affectedSOPInstanceUID; uint32_t messageID; @@ -168,7 +169,7 @@ FromDcmtkBridge::Convert(summary, **imageDataSet); FromDcmtkBridge::ToJson(dicomJson, **imageDataSet); - if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer, *imageDataSet)) + if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer, **imageDataSet)) { LOG(ERROR) << "cannot write DICOM file to memory"; rsp->DimseStatus = STATUS_STORE_Refused_OutOfResources; @@ -181,7 +182,7 @@ // check the image to make sure it is consistent, i.e. that its sopClass and sopInstance correspond // to those mentioned in the request. If not, set the status in the response message variable. - if ((rsp->DimseStatus == STATUS_Success)) + if (rsp->DimseStatus == STATUS_Success) { // which SOP class and SOP instance ? if (!DU_findSOPClassAndInstanceInDataSet(*imageDataSet, sopClass, sopInstance, /*opt_correctUIDPadding*/ OFFalse)) @@ -201,7 +202,7 @@ { try { - cbdata->handler->Handle(buffer, summary, dicomJson, cbdata->distantAET); + cbdata->handler->Handle(buffer, summary, dicomJson, cbdata->remoteAET, cbdata->calledAET); } catch (OrthancException& e) { @@ -255,11 +256,13 @@ callbackData.messageID = req->MessageID; if (assoc && assoc->params) { - callbackData.distantAET = assoc->params->DULparams.callingAPTitle; + callbackData.remoteAET = assoc->params->DULparams.callingAPTitle; + callbackData.calledAET = assoc->params->DULparams.calledAPTitle; } else { - callbackData.distantAET = ""; + callbackData.remoteAET = ""; + callbackData.calledAET = ""; } DcmFileFormat dcmff;
--- a/OrthancServer/Internals/StoreScp.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/Internals/StoreScp.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancServer/OrthancFindRequestHandler.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/OrthancFindRequestHandler.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -41,160 +41,12 @@ #include "OrthancInitialization.h" #include "FromDcmtkBridge.h" -namespace Orthanc -{ - static bool IsWildcard(const std::string& constraint) - { - return (constraint.find('-') != std::string::npos || - constraint.find('*') != std::string::npos || - constraint.find('\\') != std::string::npos || - constraint.find('?') != std::string::npos); - } - - static bool ApplyRangeConstraint(const std::string& value, - const std::string& constraint) - { - size_t separator = constraint.find('-'); - std::string lower, upper, v; - Toolbox::ToLowerCase(lower, constraint.substr(0, separator)); - Toolbox::ToLowerCase(upper, constraint.substr(separator + 1)); - Toolbox::ToLowerCase(v, value); - - if (lower.size() == 0 && upper.size() == 0) - { - return false; - } - - if (lower.size() == 0) - { - return v <= upper; - } - - if (upper.size() == 0) - { - return v >= lower; - } - - return (v >= lower && v <= upper); - } - - - static bool ApplyListConstraint(const std::string& value, - const std::string& constraint) - { - std::string v1; - Toolbox::ToLowerCase(v1, value); - - std::vector<std::string> items; - Toolbox::TokenizeString(items, constraint, '\\'); - - for (size_t i = 0; i < items.size(); i++) - { - std::string lower; - Toolbox::ToLowerCase(lower, items[i]); - if (lower == v1) - { - return true; - } - } - - return false; - } +#include "ResourceFinder.h" +#include "DicomFindQuery.h" - static bool Matches(const std::string& value, - const std::string& constraint) - { - // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained - // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html - - if (constraint.find('-') != std::string::npos) - { - return ApplyRangeConstraint(value, constraint); - } - - if (constraint.find('\\') != std::string::npos) - { - return ApplyListConstraint(value, constraint); - } - - if (constraint.find('*') != std::string::npos || - constraint.find('?') != std::string::npos) - { - // TODO - Cache the constructed regular expression - boost::regex pattern(Toolbox::WildcardToRegularExpression(constraint), - boost::regex::icase /* case insensitive search */); - return boost::regex_match(value, pattern); - } - else - { - std::string v, c; - Toolbox::ToLowerCase(v, value); - Toolbox::ToLowerCase(c, constraint); - return v == c; - } - } - - - static bool LookupOneInstance(std::string& result, - ServerIndex& index, - const std::string& id, - ResourceType type) - { - if (type == ResourceType_Instance) - { - result = id; - return true; - } - - std::string childId; - - { - std::list<std::string> children; - index.GetChildInstances(children, id); - - if (children.empty()) - { - return false; - } - - childId = children.front(); - } - - return LookupOneInstance(result, index, childId, GetChildResourceType(type)); - } - - - static bool Matches(const Json::Value& resource, - const DicomArray& query) - { - for (size_t i = 0; i < query.GetSize(); i++) - { - if (query.GetElement(i).GetValue().IsNull() || - query.GetElement(i).GetTag() == DICOM_TAG_QUERY_RETRIEVE_LEVEL || - query.GetElement(i).GetTag() == DICOM_TAG_SPECIFIC_CHARACTER_SET || - query.GetElement(i).GetTag() == DICOM_TAG_MODALITIES_IN_STUDY) - { - continue; - } - - std::string tag = query.GetElement(i).GetTag().Format(); - std::string value; - if (resource.isMember(tag)) - { - value = resource.get(tag, Json::arrayValue).get("Value", "").asString(); - } - - if (!Matches(value, query.GetElement(i).GetValue().AsString())) - { - return false; - } - } - - return true; - } - - +namespace Orthanc +{ static void AddAnswer(DicomFindAnswers& answers, const Json::Value& resource, const DicomArray& query) @@ -203,8 +55,15 @@ for (size_t i = 0; i < query.GetSize(); i++) { - if (query.GetElement(i).GetTag() != DICOM_TAG_QUERY_RETRIEVE_LEVEL && - query.GetElement(i).GetTag() != DICOM_TAG_SPECIFIC_CHARACTER_SET) + // Fix issue 30 (QR response missing "Query/Retrieve Level" (008,0052)) + if (query.GetElement(i).GetTag() == DICOM_TAG_QUERY_RETRIEVE_LEVEL) + { + result.SetValue(query.GetElement(i).GetTag(), query.GetElement(i).GetValue()); + } + else if (query.GetElement(i).GetTag() == DICOM_TAG_SPECIFIC_CHARACTER_SET) + { + } + else { std::string tag = query.GetElement(i).GetTag().Format(); std::string value; @@ -220,267 +79,157 @@ } } - answers.Add(result); - } - - - static bool ApplyModalitiesInStudyFilter(std::list<std::string>& filteredStudies, - const std::list<std::string>& studies, - const DicomMap& input, - ServerIndex& index) - { - filteredStudies.clear(); - - const DicomValue& v = input.GetValue(DICOM_TAG_MODALITIES_IN_STUDY); - if (v.IsNull()) - { - return false; - } - - // Move the allowed modalities into a "std::set" - std::vector<std::string> tmp; - Toolbox::TokenizeString(tmp, v.AsString(), '\\'); - - std::set<std::string> modalities; - for (size_t i = 0; i < tmp.size(); i++) - { - modalities.insert(tmp[i]); - } - - // Loop over the studies - for (std::list<std::string>::const_iterator - it = studies.begin(); it != studies.end(); ++it) + if (result.GetSize() == 0) { - try - { - // We are considering a single study. Check whether one of - // its child series matches one of the modalities. - Json::Value study; - if (index.LookupResource(study, *it, ResourceType_Study)) - { - // Loop over the series of the considered study. - for (Json::Value::ArrayIndex j = 0; j < study["Series"].size(); j++) // (*) - { - Json::Value series; - if (index.LookupResource(series, study["Series"][j].asString(), ResourceType_Series)) - { - // Get the modality of this series - if (series["MainDicomTags"].isMember("Modality")) - { - std::string modality = series["MainDicomTags"]["Modality"].asString(); - if (modalities.find(modality) != modalities.end()) - { - // This series of the considered study matches one - // of the required modalities. Take the study into - // consideration for future filtering. - filteredStudies.push_back(*it); - - // We have finished considering this study. Break the study loop at (*). - break; - } - } - } - } - } - } - catch (OrthancException&) - { - // This resource has probably been deleted during the find request - } + LOG(WARNING) << "The C-FIND request does not return any DICOM tag"; } - - return true; + else + { + answers.Add(result); + } } namespace { - class CandidateResources + class CFindQuery : public DicomFindQuery { private: - ServerIndex& index_; - ModalityManufacturer manufacturer_; - ResourceType level_; - bool isFilterApplied_; - std::set<std::string> filtered_; - - static void ListToSet(std::set<std::string>& target, - const std::list<std::string>& source) - { - for (std::list<std::string>::const_iterator - it = source.begin(); it != source.end(); ++it) - { - target.insert(*it); - } - } - - void ApplyExactFilter(const DicomTag& tag, const std::string& value) - { - LOG(INFO) << "Applying exact filter on tag " - << FromDcmtkBridge::GetName(tag) << " (value: " << value << ")"; - - std::list<std::string> resources; - index_.LookupTagValue(resources, tag, value, level_); - - if (isFilterApplied_) - { - std::set<std::string> s; - ListToSet(s, resources); - - std::set<std::string> tmp = filtered_; - filtered_.clear(); - - for (std::set<std::string>::const_iterator - it = tmp.begin(); it != tmp.end(); ++it) - { - if (s.find(*it) != s.end()) - { - filtered_.insert(*it); - } - } - } - else - { - assert(filtered_.empty()); - isFilterApplied_ = true; - ListToSet(filtered_, resources); - } - } + DicomFindAnswers& answers_; + ServerIndex& index_; + const DicomArray& query_; + bool hasModalitiesInStudy_; + std::set<std::string> modalitiesInStudy_; public: - CandidateResources(ServerIndex& index, - ModalityManufacturer manufacturer) : - index_(index), - manufacturer_(manufacturer), - level_(ResourceType_Patient), - isFilterApplied_(false) + CFindQuery(DicomFindAnswers& answers, + ServerIndex& index, + const DicomArray& query) : + answers_(answers), + index_(index), + query_(query), + hasModalitiesInStudy_(false) { } - ResourceType GetLevel() const - { - return level_; - } - - void GoDown() + void SetModalitiesInStudy(const std::string& value) { - assert(level_ != ResourceType_Instance); - - if (isFilterApplied_) - { - std::set<std::string> tmp = filtered_; - - filtered_.clear(); + hasModalitiesInStudy_ = true; + + std::vector<std::string> tmp; + Toolbox::TokenizeString(tmp, value, '\\'); - for (std::set<std::string>::const_iterator - it = tmp.begin(); it != tmp.end(); ++it) - { - std::list<std::string> children; - index_.GetChildren(children, *it); - ListToSet(filtered_, children); - } - } - - switch (level_) + for (size_t i = 0; i < tmp.size(); i++) { - case ResourceType_Patient: - level_ = ResourceType_Study; - break; - - case ResourceType_Study: - level_ = ResourceType_Series; - break; - - case ResourceType_Series: - level_ = ResourceType_Instance; - break; - - default: - throw OrthancException(ErrorCode_InternalError); + modalitiesInStudy_.insert(tmp[i]); } } - void Flatten(std::list<std::string>& resources) const + virtual bool HasMainDicomTagsFilter(ResourceType level) const { - resources.clear(); - - if (isFilterApplied_) + if (DicomFindQuery::HasMainDicomTagsFilter(level)) { - for (std::set<std::string>::const_iterator - it = filtered_.begin(); it != filtered_.end(); ++it) - { - resources.push_back(*it); - } + return true; } - else - { - Json::Value tmp; - index_.GetAllUuids(tmp, level_); - for (Json::Value::ArrayIndex i = 0; i < tmp.size(); i++) - { - resources.push_back(tmp[i].asString()); - } - } + + return (level == ResourceType_Study && + hasModalitiesInStudy_); } - void ApplyFilter(const DicomTag& tag, const DicomMap& query) + virtual bool FilterMainDicomTags(const std::string& resourceId, + ResourceType level, + const DicomMap& mainTags) const { - if (query.HasTag(tag)) + if (!DicomFindQuery::FilterMainDicomTags(resourceId, level, mainTags)) + { + return false; + } + + if (level != ResourceType_Study || + !hasModalitiesInStudy_) + { + return true; + } + + try { - const DicomValue& value = query.GetValue(tag); - if (!value.IsNull()) + // We are considering a single study, and the + // "MODALITIES_IN_STUDY" tag is set in the C-Find. Check + // whether one of its child series matches one of the + // modalities. + + Json::Value study; + if (index_.LookupResource(study, resourceId, ResourceType_Study)) { - std::string value = query.GetValue(tag).AsString(); - if (!IsWildcard(value)) + // Loop over the series of the considered study. + for (Json::Value::ArrayIndex j = 0; j < study["Series"].size(); j++) { - ApplyExactFilter(tag, value); + Json::Value series; + if (index_.LookupResource(series, study["Series"][j].asString(), ResourceType_Series)) + { + // Get the modality of this series + if (series["MainDicomTags"].isMember("Modality")) + { + std::string modality = series["MainDicomTags"]["Modality"].asString(); + if (modalitiesInStudy_.find(modality) != modalitiesInStudy_.end()) + { + // This series of the considered study matches one + // of the required modalities. Take the study into + // consideration for future filtering. + return true; + } + } + } } } } + catch (OrthancException&) + { + // This resource has probably been deleted during the find request + } + + return false; + } + + virtual bool HasInstanceFilter() const + { + return true; + } + + virtual bool FilterInstance(const std::string& instanceId, + const Json::Value& content) const + { + bool ok = DicomFindQuery::FilterInstance(instanceId, content); + + if (ok) + { + // Add this resource to the answers + AddAnswer(answers_, content, query_); + } + + return ok; } }; } - bool OrthancFindRequestHandler::HasReachedLimit(const DicomFindAnswers& answers, - ResourceType level) const - { - switch (level) - { - case ResourceType_Patient: - case ResourceType_Study: - case ResourceType_Series: - return (maxResults_ != 0 && answers.GetSize() >= maxResults_); - - case ResourceType_Instance: - return (maxInstances_ != 0 && answers.GetSize() >= maxInstances_); - - default: - throw OrthancException(ErrorCode_InternalError); - } - } - bool OrthancFindRequestHandler::Handle(DicomFindAnswers& answers, const DicomMap& input, const std::string& callingAETitle) { /** - * Retrieve the manufacturer of this modality. + * Ensure that the calling modality is known to Orthanc. **/ - ModalityManufacturer manufacturer; - - { - RemoteModalityParameters modality; + RemoteModalityParameters modality; - if (!Configuration::LookupDicomModalityUsingAETitle(modality, callingAETitle)) - { - throw OrthancException("Unknown modality"); - } + if (!Configuration::LookupDicomModalityUsingAETitle(modality, callingAETitle)) + { + throw OrthancException("Unknown modality"); + } - manufacturer = modality.GetManufacturer(); - } + // ModalityManufacturer manufacturer = modality.GetManufacturer(); /** @@ -519,114 +268,63 @@ /** - * Retrieve the candidate resources for this query level. Whenever - * possible, we avoid returning ALL the resources for this query - * level, as it would imply reading the JSON file on the harddisk - * for each of them. + * Build up the query object. **/ - CandidateResources candidates(context_.GetIndex(), manufacturer); - - for (;;) + CFindQuery findQuery(answers, context_.GetIndex(), query); + findQuery.SetLevel(level); + + for (size_t i = 0; i < query.GetSize(); i++) { - switch (candidates.GetLevel()) - { - case ResourceType_Patient: - candidates.ApplyFilter(DICOM_TAG_PATIENT_ID, input); - break; - - case ResourceType_Study: - candidates.ApplyFilter(DICOM_TAG_STUDY_INSTANCE_UID, input); - candidates.ApplyFilter(DICOM_TAG_ACCESSION_NUMBER, input); - break; + const DicomTag tag = query.GetElement(i).GetTag(); - case ResourceType_Series: - candidates.ApplyFilter(DICOM_TAG_SERIES_INSTANCE_UID, input); - break; - - case ResourceType_Instance: - candidates.ApplyFilter(DICOM_TAG_SOP_INSTANCE_UID, input); - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - if (candidates.GetLevel() == level) + if (query.GetElement(i).GetValue().IsNull() || + tag == DICOM_TAG_QUERY_RETRIEVE_LEVEL || + tag == DICOM_TAG_SPECIFIC_CHARACTER_SET) { - break; + continue; } - candidates.GoDown(); - } - - std::list<std::string> resources; - candidates.Flatten(resources); - - LOG(INFO) << "Number of candidate resources after exact filtering: " << resources.size(); + std::string value = query.GetElement(i).GetValue().AsString(); - /** - * Apply filtering on modalities for studies, if asked (this is an - * extension to standard DICOM) - * http://www.medicalconnections.co.uk/kb/Filtering_on_and_Retrieving_the_Modality_in_a_C_FIND - **/ - - if (level == ResourceType_Study && - input.HasTag(DICOM_TAG_MODALITIES_IN_STUDY)) - { - std::list<std::string> filtered; - if (ApplyModalitiesInStudyFilter(filtered, resources, input, context_.GetIndex())) + if (tag == DICOM_TAG_MODALITIES_IN_STUDY) { - resources = filtered; + findQuery.SetModalitiesInStudy(value); + } + else + { + findQuery.SetConstraint(tag, value); } } /** - * Loop over all the resources for this query level. + * Run the query. **/ - for (std::list<std::string>::const_iterator - resource = resources.begin(); resource != resources.end(); ++resource) + ResourceFinder finder(context_); + + switch (level) { - try - { - std::string instance; - if (LookupOneInstance(instance, context_.GetIndex(), *resource, level)) - { - Json::Value info; - context_.ReadJson(info, instance); - - if (Matches(info, query)) - { - if (HasReachedLimit(answers, level)) - { - // Too many results, stop before recording this new match - return false; - } + case ResourceType_Patient: + case ResourceType_Study: + case ResourceType_Series: + finder.SetMaxResults(maxResults_); + break; - AddAnswer(answers, info, query); - } - } - } - catch (OrthancException&) - { - // This resource has probably been deleted during the find request - } + case ResourceType_Instance: + finder.SetMaxResults(maxInstances_); + break; + + default: + throw OrthancException(ErrorCode_InternalError); } - return true; // All the matching resources have been returned + std::list<std::string> tmp; + bool finished = finder.Apply(tmp, findQuery); + + LOG(INFO) << "Number of matching resources: " << tmp.size(); + + return finished; } } - - - -/** - * TODO : Case-insensitive match for PN value representation (Patient - * Name). Case-senstive match for all the other value representations. - * - * Reference: DICOM PS 3.4 - * - C.2.2.2.1 ("Single Value Matching") - * - C.2.2.2.4 ("Wild Card Matching") - * http://medical.nema.org/Dicom/2011/11_04pu.pdf ( - **/
--- a/OrthancServer/OrthancFindRequestHandler.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/OrthancFindRequestHandler.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancServer/OrthancInitialization.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/OrthancInitialization.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -45,6 +45,9 @@ #include <boost/thread.hpp> #include <glog/logging.h> +#include "DatabaseWrapper.h" +#include "../Core/FileStorage/FilesystemStorage.h" + #if ORTHANC_JPEG_ENABLED == 1 #include <dcmtk/dcmjpeg/djdecode.h> @@ -61,6 +64,7 @@ static boost::mutex globalMutex_; static std::auto_ptr<Json::Value> configuration_; static boost::filesystem::path defaultDirectory_; + static std::string configurationAbsolutePath_; static void ReadGlobalConfiguration(const char* configurationFile) @@ -74,6 +78,7 @@ Toolbox::ReadFile(content, configurationFile); defaultDirectory_ = boost::filesystem::path(configurationFile).parent_path(); LOG(WARNING) << "Using the configuration from: " << configurationFile; + configurationAbsolutePath_ = boost::filesystem::absolute(configurationFile).string(); } else { @@ -93,6 +98,7 @@ p /= "Configuration.json"; Toolbox::ReadFile(content, p.string()); LOG(WARNING) << "Using the configuration from: " << p.string(); + configurationAbsolutePath_ = boost::filesystem::absolute(p).string(); } catch (OrthancException&) { @@ -103,6 +109,7 @@ #endif } + Json::Reader reader; if (!reader.parse(content, *configuration_)) { @@ -288,7 +295,8 @@ if (modalities.type() != Json::objectValue || !modalities.isMember(name)) { - throw OrthancException(ErrorCode_BadFileFormat); + LOG(ERROR) << "No modality with symbolic name: " << name; + throw OrthancException(ErrorCode_InexistentItem); } try @@ -321,7 +329,8 @@ if (modalities.type() != Json::objectValue || !modalities.isMember(name)) { - throw OrthancException(ErrorCode_BadFileFormat); + LOG(ERROR) << "No peer with symbolic name: " << name; + throw OrthancException(ErrorCode_InexistentItem); } peer.FromJson(modalities[name]); @@ -641,4 +650,115 @@ peers.removeMember(symbolicName.c_str()); } + + + + const std::string& Configuration::GetConfigurationAbsolutePath() + { + return configurationAbsolutePath_; + } + + + static IDatabaseWrapper* CreateSQLiteWrapper() + { + std::string storageDirectoryStr = Configuration::GetGlobalStringParameter("StorageDirectory", "OrthancStorage"); + + // Open the database + boost::filesystem::path indexDirectory = Configuration::InterpretStringParameterAsPath( + Configuration::GetGlobalStringParameter("IndexDirectory", storageDirectoryStr)); + + LOG(WARNING) << "SQLite index directory: " << indexDirectory; + + try + { + boost::filesystem::create_directories(indexDirectory); + } + catch (boost::filesystem::filesystem_error) + { + } + + return new DatabaseWrapper(indexDirectory.string() + "/index"); + } + + + namespace + { + // Anonymous namespace to avoid clashes between compilation modules + + class FilesystemStorageWithoutDicom : public IStorageArea + { + private: + FilesystemStorage storage_; + + public: + FilesystemStorageWithoutDicom(const std::string& path) : storage_(path) + { + } + + virtual void Create(const std::string& uuid, + const void* content, + size_t size, + FileContentType type) + { + if (type != FileContentType_Dicom) + { + storage_.Create(uuid, content, size, type); + } + } + + virtual void Read(std::string& content, + const std::string& uuid, + FileContentType type) + { + if (type != FileContentType_Dicom) + { + storage_.Read(content, uuid, type); + } + else + { + throw OrthancException(ErrorCode_UnknownResource); + } + } + + virtual void Remove(const std::string& uuid, + FileContentType type) + { + if (type != FileContentType_Dicom) + { + storage_.Remove(uuid, type); + } + } + }; + } + + + static IStorageArea* CreateFilesystemStorage() + { + std::string storageDirectoryStr = Configuration::GetGlobalStringParameter("StorageDirectory", "OrthancStorage"); + + boost::filesystem::path storageDirectory = Configuration::InterpretStringParameterAsPath(storageDirectoryStr); + LOG(WARNING) << "Storage directory: " << storageDirectory; + + if (Configuration::GetGlobalBoolParameter("StoreDicom", true)) + { + return new FilesystemStorage(storageDirectory.string()); + } + else + { + LOG(WARNING) << "The DICOM files will not be stored, Orthanc running in index-only mode"; + return new FilesystemStorageWithoutDicom(storageDirectory.string()); + } + } + + + IDatabaseWrapper* Configuration::CreateDatabaseWrapper() + { + return CreateSQLiteWrapper(); + } + + + IStorageArea* Configuration::CreateStorageArea() + { + return CreateFilesystemStorage(); + } }
--- a/OrthancServer/OrthancInitialization.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/OrthancInitialization.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -40,6 +40,8 @@ #include "DicomProtocol/RemoteModalityParameters.h" #include "ServerEnumerations.h" #include "OrthancPeerParameters.h" +#include "IDatabaseWrapper.h" +#include "../Core/FileStorage/IStorageArea.h" namespace Orthanc { @@ -100,5 +102,11 @@ const OrthancPeerParameters& peer); static void RemovePeer(const std::string& symbolicName); + + static const std::string& GetConfigurationAbsolutePath(); + + static IDatabaseWrapper* CreateDatabaseWrapper(); + + static IStorageArea* CreateStorageArea(); }; }
--- a/OrthancServer/OrthancMoveRequestHandler.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/OrthancMoveRequestHandler.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -101,9 +101,9 @@ } - bool OrthancMoveRequestHandler::LookupResource(std::string& publicId, - DicomTag tag, - const DicomMap& input) + bool OrthancMoveRequestHandler::LookupIdentifier(std::string& publicId, + DicomTag tag, + const DicomMap& input) { if (!input.HasTag(tag)) { @@ -113,7 +113,7 @@ std::string value = input.GetValue(tag).AsString(); std::list<std::string> ids; - context_.GetIndex().LookupTagValue(ids, tag, value); + context_.GetIndex().LookupIdentifier(ids, tag, value); if (ids.size() != 1) { @@ -132,18 +132,42 @@ { LOG(WARNING) << "Move-SCU request received for AET \"" << aet << "\""; - /** * Retrieve the query level. **/ + ResourceType level; const DicomValue* levelTmp = input.TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL); - if (levelTmp == NULL) + + if (levelTmp != NULL) + { + level = StringToResourceType(levelTmp->AsString().c_str()); + } + else { - throw OrthancException(ErrorCode_BadRequest); + // The query level is not present in the C-Move request, which + // does not follow the DICOM standard. This is for instance the + // behavior of Tudor DICOM. Try and automatically deduce the + // query level: Start from the instance level, going up to the + // patient level until a valid DICOM identifier is found. + + std::string publicId; + + if (LookupIdentifier(publicId, DICOM_TAG_SOP_INSTANCE_UID, input) || + LookupIdentifier(publicId, DICOM_TAG_SERIES_INSTANCE_UID, input) || + LookupIdentifier(publicId, DICOM_TAG_STUDY_INSTANCE_UID, input) || + LookupIdentifier(publicId, DICOM_TAG_PATIENT_ID, input)) + { + return new OrthancMoveRequestIterator(context_, aet, publicId); + } + else + { + // No identifier is present in the request. + throw OrthancException(ErrorCode_BadRequest); + } } - ResourceType level = StringToResourceType(levelTmp->AsString().c_str()); + /** * Lookup for the resource to be sent. @@ -155,19 +179,19 @@ switch (level) { case ResourceType_Patient: - ok = LookupResource(publicId, DICOM_TAG_PATIENT_ID, input); + ok = LookupIdentifier(publicId, DICOM_TAG_PATIENT_ID, input); break; case ResourceType_Study: - ok = LookupResource(publicId, DICOM_TAG_STUDY_INSTANCE_UID, input); + ok = LookupIdentifier(publicId, DICOM_TAG_STUDY_INSTANCE_UID, input); break; case ResourceType_Series: - ok = LookupResource(publicId, DICOM_TAG_SERIES_INSTANCE_UID, input); + ok = LookupIdentifier(publicId, DICOM_TAG_SERIES_INSTANCE_UID, input); break; case ResourceType_Instance: - ok = LookupResource(publicId, DICOM_TAG_SOP_INSTANCE_UID, input); + ok = LookupIdentifier(publicId, DICOM_TAG_SOP_INSTANCE_UID, input); break; default:
--- a/OrthancServer/OrthancMoveRequestHandler.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/OrthancMoveRequestHandler.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -41,9 +41,9 @@ private: ServerContext& context_; - bool LookupResource(std::string& publicId, - DicomTag tag, - const DicomMap& input); + bool LookupIdentifier(std::string& publicId, + DicomTag tag, + const DicomMap& input); public: OrthancMoveRequestHandler(ServerContext& context) :
--- a/OrthancServer/OrthancPeerParameters.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/OrthancPeerParameters.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancServer/OrthancPeerParameters.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/OrthancPeerParameters.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -33,8 +33,8 @@ #include "../PrecompiledHeadersServer.h" #include "OrthancRestApi.h" -#include "../DicomModification.h" #include "../FromDcmtkBridge.h" +#include "../../Core/Uuid.h" #include <glog/logging.h> @@ -111,13 +111,11 @@ } - static bool ParseModifyRequest(DicomModification& target, - const RestApi::PostCall& call) + + bool OrthancRestApi::ParseModifyRequest(DicomModification& target, + const Json::Value& request) { - // curl http://localhost:8042/series/95a6e2bf-9296e2cc-bf614e2f-22b391ee-16e010e0/modify -X POST -d '{"Replace":{"InstitutionName":"My own clinic"}}' - - Json::Value request; - if (call.ParseJsonRequest(request) && request.isObject()) + if (request.isObject()) { if (request.isMember("RemovePrivateTags")) { @@ -143,8 +141,25 @@ } + static bool ParseModifyRequest(DicomModification& target, + const RestApiPostCall& call) + { + // curl http://localhost:8042/series/95a6e2bf-9296e2cc-bf614e2f-22b391ee-16e010e0/modify -X POST -d '{"Replace":{"InstitutionName":"My own clinic"}}' + + Json::Value request; + if (call.ParseJsonRequest(request)) + { + return OrthancRestApi::ParseModifyRequest(target, request); + } + else + { + return false; + } + } + + static bool ParseAnonymizationRequest(DicomModification& target, - RestApi::PostCall& call) + RestApiPostCall& call) { // curl http://localhost:8042/instances/6e67da51-d119d6ae-c5667437-87b9a8a5-0f07c49f/anonymize -X POST -d '{"Replace":{"PatientName":"hello","0010-0020":"world"},"Keep":["StudyDescription", "SeriesDescription"],"KeepPrivateTags": null,"Remove":["Modality"]}' > Anonymized.dcm @@ -174,11 +189,12 @@ ParseListOfTags(target, request["Keep"], TagOperation_Keep); } - if (target.GetReplacement(DICOM_TAG_PATIENT_NAME) == patientName) + if (target.IsReplaced(DICOM_TAG_PATIENT_NAME) && + target.GetReplacement(DICOM_TAG_PATIENT_NAME) == patientName) { // Overwrite the random Patient's Name by one that is more // user-friendly (provided none was specified by the user) - target.Replace(DICOM_TAG_PATIENT_NAME, GeneratePatientName(OrthancRestApi::GetContext(call))); + target.Replace(DICOM_TAG_PATIENT_NAME, GeneratePatientName(OrthancRestApi::GetContext(call)), true); } return true; @@ -191,7 +207,7 @@ static void AnonymizeOrModifyInstance(DicomModification& modification, - RestApi::PostCall& call) + RestApiPostCall& call) { std::string id = call.GetUriComponent("id", ""); @@ -207,7 +223,7 @@ MetadataType metadataType, ChangeType changeType, ResourceType resourceType, - RestApi::PostCall& call) + RestApiPostCall& call) { bool isFirst = true; Json::Value result(Json::objectValue); @@ -246,52 +262,61 @@ continue; } + ParsedDicomFile& original = locker->GetDicom(); DicomInstanceHasher originalHasher = original.GetHasher(); /** - * Compute the resulting DICOM instance and store it into the Orthanc store. + * Compute the resulting DICOM instance. **/ std::auto_ptr<ParsedDicomFile> modified(original.Clone()); modification.Apply(*modified); - std::string modifiedInstance; - if (context.Store(modifiedInstance, *modified) != StoreStatus_Success) - { - LOG(ERROR) << "Error while storing a modified instance " << *it; - return; - } + DicomInstanceToStore toStore; + toStore.SetParsedDicomFile(*modified); /** - * Record metadata information (AnonymizedFrom/ModifiedFrom). + * Prepare the metadata information to associate with the + * resulting DICOM instance (AnonymizedFrom/ModifiedFrom). **/ DicomInstanceHasher modifiedHasher = modified->GetHasher(); if (originalHasher.HashSeries() != modifiedHasher.HashSeries()) { - context.GetIndex().SetMetadata(modifiedHasher.HashSeries(), - metadataType, originalHasher.HashSeries()); + toStore.AddMetadata(ResourceType_Series, metadataType, originalHasher.HashSeries()); } if (originalHasher.HashStudy() != modifiedHasher.HashStudy()) { - context.GetIndex().SetMetadata(modifiedHasher.HashStudy(), - metadataType, originalHasher.HashStudy()); + toStore.AddMetadata(ResourceType_Study, metadataType, originalHasher.HashStudy()); } if (originalHasher.HashPatient() != modifiedHasher.HashPatient()) { - context.GetIndex().SetMetadata(modifiedHasher.HashPatient(), - metadataType, originalHasher.HashPatient()); + toStore.AddMetadata(ResourceType_Patient, metadataType, originalHasher.HashPatient()); } assert(*it == originalHasher.HashInstance()); + toStore.AddMetadata(ResourceType_Instance, metadataType, *it); + + + /** + * Store the resulting DICOM instance into the Orthanc store. + **/ + + std::string modifiedInstance; + if (context.Store(modifiedInstance, toStore) != StoreStatus_Success) + { + LOG(ERROR) << "Error while storing a modified instance " << *it; + return; + } + + // Sanity checks in debug mode assert(modifiedInstance == modifiedHasher.HashInstance()); - context.GetIndex().SetMetadata(modifiedInstance, metadataType, *it); /** @@ -333,9 +358,10 @@ - static void ModifyInstance(RestApi::PostCall& call) + static void ModifyInstance(RestApiPostCall& call) { DicomModification modification; + modification.SetAllowManualIdentifiers(true); if (ParseModifyRequest(modification, call)) { @@ -361,9 +387,10 @@ } - static void AnonymizeInstance(RestApi::PostCall& call) + static void AnonymizeInstance(RestApiPostCall& call) { DicomModification modification; + modification.SetAllowManualIdentifiers(true); if (ParseAnonymizationRequest(modification, call)) { @@ -374,7 +401,7 @@ template <enum ChangeType changeType, enum ResourceType resourceType> - static void ModifyResource(RestApi::PostCall& call) + static void ModifyResource(RestApiPostCall& call) { DicomModification modification; @@ -389,7 +416,7 @@ template <enum ChangeType changeType, enum ResourceType resourceType> - static void AnonymizeResource(RestApi::PostCall& call) + static void AnonymizeResource(RestApiPostCall& call) { DicomModification modification; @@ -401,29 +428,38 @@ } - static void Create(RestApi::PostCall& call) + static void CreateDicom(RestApiPostCall& call) { // curl http://localhost:8042/tools/create-dicom -X POST -d '{"PatientName":"Hello^World"}' // curl http://localhost:8042/tools/create-dicom -X POST -d '{"PatientName":"Hello^World","PixelData":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDDcB53FulQAAAElJREFUGNNtj0sSAEEEQ1+U+185s1CtmRkblQ9CZldsKHJDk6DLGLJa6chjh0ooQmpjXMM86zPwydGEj6Ed/UGykkEM8X+p3u8/8LcOJIWLGeMAAAAASUVORK5CYII="}' - Json::Value request; - if (call.ParseJsonRequest(request) && request.isObject()) + Json::Value replacements; + if (call.ParseJsonRequest(replacements) && replacements.isObject()) { - DicomModification modification; - ParseReplacements(modification, request); - ParsedDicomFile dicom; - if (modification.IsReplaced(DICOM_TAG_PIXEL_DATA)) + Json::Value::Members members = replacements.getMemberNames(); + for (size_t i = 0; i < members.size(); i++) { - dicom.EmbedImage(modification.GetReplacement(DICOM_TAG_PIXEL_DATA)); - modification.Keep(DICOM_TAG_PIXEL_DATA); + const std::string& name = members[i]; + std::string value = replacements[name].asString(); + + DicomTag tag = FromDcmtkBridge::ParseTag(name); + if (tag == DICOM_TAG_PIXEL_DATA) + { + dicom.EmbedImage(value); + } + else + { + dicom.Replace(tag, value); + } } - modification.Apply(dicom); + DicomInstanceToStore toStore; + toStore.SetParsedDicomFile(dicom); std::string id; - StoreStatus status = OrthancRestApi::GetContext(call).Store(id, dicom); + StoreStatus status = OrthancRestApi::GetContext(call).Store(id, toStore); if (status == StoreStatus_Failure) { @@ -448,6 +484,6 @@ Register("/studies/{id}/anonymize", AnonymizeResource<ChangeType_ModifiedStudy, ResourceType_Study>); Register("/patients/{id}/anonymize", AnonymizeResource<ChangeType_ModifiedPatient, ResourceType_Patient>); - Register("/tools/create-dicom", Create); + Register("/tools/create-dicom", CreateDicom); } }
--- a/OrthancServer/OrthancRestApi/OrthancRestApi.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestApi.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -39,9 +39,9 @@ namespace Orthanc { - void OrthancRestApi::AnswerStoredInstance(RestApi::PostCall& call, + void OrthancRestApi::AnswerStoredInstance(RestApiPostCall& call, const std::string& publicId, - StoreStatus status) + StoreStatus status) const { Json::Value result = Json::objectValue; @@ -56,10 +56,19 @@ } + void OrthancRestApi::ResetOrthanc(RestApiPostCall& call) + { + OrthancRestApi::GetApi(call).resetRequestReceived_ = true; + call.GetOutput().AnswerBuffer("{}", "application/json"); + } + + + + // Upload of DICOM files through HTTP --------------------------------------- - static void UploadDicomFile(RestApi::PostCall& call) + static void UploadDicomFile(RestApiPostCall& call) { ServerContext& context = OrthancRestApi::GetContext(call); @@ -71,8 +80,11 @@ LOG(INFO) << "Receiving a DICOM file of " << postData.size() << " bytes through HTTP"; + DicomInstanceToStore toStore; + toStore.SetBuffer(postData); + std::string publicId; - StoreStatus status = context.Store(publicId, postData); + StoreStatus status = context.Store(publicId, toStore); OrthancRestApi::GetApi(call).AnswerStoredInstance(call, publicId, status); } @@ -82,7 +94,8 @@ // Registration of the various REST handlers -------------------------------- OrthancRestApi::OrthancRestApi(ServerContext& context) : - context_(context) + context_(context), + resetRequestReceived_(false) { RegisterSystem(); @@ -93,5 +106,10 @@ RegisterArchive(); Register("/instances", UploadDicomFile); + + // Auto-generated directories + Register("/tools", RestApi::AutoListChildren); + Register("/tools/reset", ResetOrthanc); + Register("/instances/{id}/frames/{frame}", RestApi::AutoListChildren); } }
--- a/OrthancServer/OrthancRestApi/OrthancRestApi.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestApi.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -32,8 +32,9 @@ #pragma once +#include "../../Core/RestApi/RestApi.h" +#include "../DicomModification.h" #include "../ServerContext.h" -#include "../../Core/RestApi/RestApi.h" #include <set> @@ -46,6 +47,7 @@ private: ServerContext& context_; + bool resetRequestReceived_; void RegisterSystem(); @@ -59,26 +61,36 @@ void RegisterArchive(); + static void ResetOrthanc(RestApiPostCall& call); + public: OrthancRestApi(ServerContext& context); - static OrthancRestApi& GetApi(RestApi::Call& call) + const bool& ResetRequestReceivedFlag() const + { + return resetRequestReceived_; + } + + static OrthancRestApi& GetApi(RestApiCall& call) { return dynamic_cast<OrthancRestApi&>(call.GetContext()); } - static ServerContext& GetContext(RestApi::Call& call) + static ServerContext& GetContext(RestApiCall& call) { return GetApi(call).context_; } - static ServerIndex& GetIndex(RestApi::Call& call) + static ServerIndex& GetIndex(RestApiCall& call) { return GetContext(call).GetIndex(); } - void AnswerStoredInstance(RestApi::PostCall& call, + void AnswerStoredInstance(RestApiPostCall& call, const std::string& publicId, - StoreStatus status); + StoreStatus status) const; + + static bool ParseModifyRequest(DicomModification& target, + const Json::Value& request); }; }
--- a/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -33,11 +33,13 @@ #include "../PrecompiledHeadersServer.h" #include "OrthancRestApi.h" +#include "../DicomDirWriter.h" #include "../../Core/Compression/HierarchicalZipWriter.h" #include "../../Core/HttpServer/FilesystemHttpSender.h" #include "../../Core/Uuid.h" #include <glog/logging.h> +#include <stdio.h> #if defined(_MSC_VER) #define snprintf _snprintf @@ -53,30 +55,45 @@ static std::string GetDirectoryNameInArchive(const Json::Value& resource, ResourceType resourceType) { + std::string s; + const Json::Value& tags = resource["MainDicomTags"]; + switch (resourceType) { case ResourceType_Patient: { - std::string p = resource["MainDicomTags"]["PatientID"].asString(); - std::string n = resource["MainDicomTags"]["PatientName"].asString(); - return p + " " + n; + std::string p = tags["PatientID"].asString(); + std::string n = tags["PatientName"].asString(); + s = p + " " + n; + break; } case ResourceType_Study: { - return resource["MainDicomTags"]["StudyDescription"].asString(); + std::string p; + if (tags.isMember("AccessionNumber")) + { + p = tags["AccessionNumber"].asString() + " "; + } + + s = p + tags["StudyDescription"].asString(); + break; } case ResourceType_Series: { - std::string d = resource["MainDicomTags"]["SeriesDescription"].asString(); - std::string m = resource["MainDicomTags"]["Modality"].asString(); - return m + " " + d; + std::string d = tags["SeriesDescription"].asString(); + std::string m = tags["Modality"].asString(); + s = m + " " + d; + break; } default: throw OrthancException(ErrorCode_InternalError); } + + // Get rid of special characters + return Toolbox::ConvertToAscii(s); } static bool CreateRootDirectoryInArchive(HierarchicalZipWriter& writer, @@ -125,13 +142,6 @@ const std::string& instancePublicId, const char* filename) { - Json::Value instance; - - if (!context.GetIndex().LookupResource(instance, instancePublicId, ResourceType_Instance)) - { - return false; - } - writer.OpenFile(filename); std::string dicom; @@ -232,13 +242,10 @@ return true; } - template <enum ResourceType resourceType> - static void GetArchive(RestApi::GetCall& call) + + static bool IsZip64Required(ServerIndex& index, + const std::string& id) { - ServerContext& context = OrthancRestApi::GetContext(call); - - std::string id = call.GetUriComponent("id", ""); - /** * Determine whether ZIP64 is required. Original ZIP format can * store up to 2GB of data (some implementation supporting up to @@ -251,8 +258,8 @@ unsigned int countStudies; unsigned int countSeries; unsigned int countInstances; - context.GetIndex().GetStatistics(compressedSize, uncompressedSize, - countStudies, countSeries, countInstances, id); + index.GetStatistics(compressedSize, uncompressedSize, + countStudies, countSeries, countInstances, id); const bool isZip64 = (uncompressedSize >= 2 * GIGA_BYTES || countInstances >= 65535); @@ -260,6 +267,18 @@ << (uncompressedSize / MEGA_BYTES) << "MB using the " << (isZip64 ? "ZIP64" : "ZIP32") << " file format"; + return isZip64; + } + + + template <enum ResourceType resourceType> + static void GetArchive(RestApiGetCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string id = call.GetUriComponent("id", ""); + bool isZip64 = IsZip64Required(context.GetIndex(), id); + // Create a RAII for the temporary file to manage the ZIP file Toolbox::TemporaryFile tmp; @@ -287,10 +306,75 @@ } + static void GetMediaArchive(RestApiGetCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string id = call.GetUriComponent("id", ""); + bool isZip64 = IsZip64Required(context.GetIndex(), id); + + // Create a RAII for the temporary file to manage the ZIP file + Toolbox::TemporaryFile tmp; + + { + // Create a ZIP writer + HierarchicalZipWriter writer(tmp.GetPath().c_str()); + writer.SetZip64(isZip64); + writer.OpenDirectory("IMAGES"); + + // Create the DICOMDIR writer + DicomDirWriter dicomDir; + + // Retrieve the list of the instances + std::list<std::string> instances; + context.GetIndex().GetChildInstances(instances, id); + + size_t pos = 0; + for (std::list<std::string>::const_iterator + it = instances.begin(); it != instances.end(); ++it, ++pos) + { + // "DICOM restricts the filenames on DICOM media to 8 + // characters (some systems wrongly use 8.3, but this does not + // conform to the standard)." + std::string filename = "IM" + boost::lexical_cast<std::string>(pos); + writer.OpenFile(filename.c_str()); + + std::string dicom; + context.ReadFile(dicom, *it, FileContentType_Dicom); + writer.Write(dicom); + + ParsedDicomFile parsed(dicom); + dicomDir.Add("IMAGES", filename, parsed); + } + + // Add the DICOMDIR + writer.CloseDirectory(); + writer.OpenFile("DICOMDIR"); + std::string s; + dicomDir.Encode(s); + writer.Write(s); + } + + // Prepare the sending of the ZIP file + FilesystemHttpSender sender(tmp.GetPath().c_str()); + sender.SetContentType("application/zip"); + sender.SetDownloadFilename(id + ".zip"); + + // Send the ZIP + call.GetOutput().AnswerFile(sender); + + // The temporary file is automatically removed thanks to the RAII + } + + void OrthancRestApi::RegisterArchive() { Register("/patients/{id}/archive", GetArchive<ResourceType_Patient>); Register("/studies/{id}/archive", GetArchive<ResourceType_Study>); Register("/series/{id}/archive", GetArchive<ResourceType_Series>); + + Register("/patients/{id}/media", GetMediaArchive); + Register("/studies/{id}/media", GetMediaArchive); + Register("/series/{id}/media", GetMediaArchive); } }
--- a/OrthancServer/OrthancRestApi/OrthancRestChanges.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestChanges.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -42,7 +42,7 @@ static void GetSinceAndLimit(int64_t& since, unsigned int& limit, bool& last, - const RestApi::GetCall& call) + const RestApiGetCall& call) { static const unsigned int MAX_RESULTS = 100; @@ -70,7 +70,7 @@ } } - static void GetChanges(RestApi::GetCall& call) + static void GetChanges(RestApiGetCall& call) { ServerContext& context = OrthancRestApi::GetContext(call); @@ -81,15 +81,20 @@ GetSinceAndLimit(since, limit, last, call); Json::Value result; - if ((!last && context.GetIndex().GetChanges(result, since, limit)) || - ( last && context.GetIndex().GetLastChange(result))) + if (last) { - call.GetOutput().AnswerJson(result); + context.GetIndex().GetLastChange(result); } + else + { + context.GetIndex().GetChanges(result, since, limit); + } + + call.GetOutput().AnswerJson(result); } - static void DeleteChanges(RestApi::DeleteCall& call) + static void DeleteChanges(RestApiDeleteCall& call) { OrthancRestApi::GetIndex(call).DeleteChanges(); call.GetOutput().AnswerBuffer("", "text/plain"); @@ -98,7 +103,7 @@ // Exports API -------------------------------------------------------------- - static void GetExports(RestApi::GetCall& call) + static void GetExports(RestApiGetCall& call) { ServerContext& context = OrthancRestApi::GetContext(call); @@ -108,15 +113,20 @@ GetSinceAndLimit(since, limit, last, call); Json::Value result; - if ((!last && context.GetIndex().GetExportedResources(result, since, limit)) || - ( last && context.GetIndex().GetLastExportedResource(result))) + if (last) { - call.GetOutput().AnswerJson(result); + context.GetIndex().GetLastExportedResource(result); } + else + { + context.GetIndex().GetExportedResources(result, since, limit); + } + + call.GetOutput().AnswerJson(result); } - static void DeleteExports(RestApi::DeleteCall& call) + static void DeleteExports(RestApiDeleteCall& call) { OrthancRestApi::GetIndex(call).DeleteExportedResources(); call.GetOutput().AnswerBuffer("", "text/plain");
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -36,6 +36,9 @@ #include "../OrthancInitialization.h" #include "../../Core/HttpClient.h" #include "../FromDcmtkBridge.h" +#include "../Scheduler/ServerJob.h" +#include "../Scheduler/StoreScuCommand.h" +#include "../Scheduler/StorePeerCommand.h" #include <glog/logging.h> @@ -65,7 +68,33 @@ return true; } - static void DicomFindPatient(RestApi::PostCall& call) + + static void DicomEcho(RestApiPostCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + RemoteModalityParameters remote = Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", "")); + ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), remote); + + try + { + if (locker.GetConnection().Echo()) + { + // Echo has succeeded + call.GetOutput().AnswerBuffer("{}", "application/json"); + return; + } + } + catch (OrthancException&) + { + } + + // Echo has failed + call.GetOutput().SignalError(HttpStatus_500_InternalServerError); + } + + + static void DicomFindPatient(RestApiPostCall& call) { ServerContext& context = OrthancRestApi::GetContext(call); @@ -87,7 +116,7 @@ call.GetOutput().AnswerJson(result); } - static void DicomFindStudy(RestApi::PostCall& call) + static void DicomFindStudy(RestApiPostCall& call) { ServerContext& context = OrthancRestApi::GetContext(call); @@ -115,7 +144,7 @@ call.GetOutput().AnswerJson(result); } - static void DicomFindSeries(RestApi::PostCall& call) + static void DicomFindSeries(RestApiPostCall& call) { ServerContext& context = OrthancRestApi::GetContext(call); @@ -144,7 +173,7 @@ call.GetOutput().AnswerJson(result); } - static void DicomFindInstance(RestApi::PostCall& call) + static void DicomFindInstance(RestApiPostCall& call) { ServerContext& context = OrthancRestApi::GetContext(call); @@ -174,7 +203,7 @@ call.GetOutput().AnswerJson(result); } - static void DicomFind(RestApi::PostCall& call) + static void DicomFind(RestApiPostCall& call) { ServerContext& context = OrthancRestApi::GetContext(call); @@ -248,7 +277,7 @@ static bool GetInstancesToExport(std::list<std::string>& instances, const std::string& remote, - RestApi::PostCall& call) + RestApiPostCall& call) { ServerContext& context = OrthancRestApi::GetContext(call); @@ -268,7 +297,11 @@ if (request.isString()) { - context.GetIndex().LogExportedResource(request.asString(), remote); + if (Configuration::GetGlobalBoolParameter("LogExportedResources", true)) + { + context.GetIndex().LogExportedResource(request.asString(), remote); + } + context.GetIndex().GetChildInstances(instances, request.asString()); } else if (request.isArray()) @@ -286,7 +319,10 @@ return false; } - context.GetIndex().LogExportedResource(stripped, remote); + if (Configuration::GetGlobalBoolParameter("LogExportedResources", true)) + { + context.GetIndex().LogExportedResource(stripped, remote); + } std::list<std::string> tmp; context.GetIndex().GetChildInstances(tmp, stripped); @@ -308,7 +344,7 @@ } - static void DicomStore(RestApi::PostCall& call) + static void DicomStore(RestApiPostCall& call) { ServerContext& context = OrthancRestApi::GetContext(call); @@ -321,19 +357,25 @@ } RemoteModalityParameters p = Configuration::GetModalityUsingSymbolicName(remote); - ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), p); + ServerJob job; for (std::list<std::string>::const_iterator it = instances.begin(); it != instances.end(); ++it) { - LOG(INFO) << "Sending resource " << *it << " to modality \"" << remote << "\""; - - std::string dicom; - context.ReadFile(dicom, *it, FileContentType_Dicom); - locker.GetConnection().Store(dicom); + job.AddCommand(new StoreScuCommand(context, p, false)).AddInput(*it); } - call.GetOutput().AnswerBuffer("{}", "application/json"); + job.SetDescription("HTTP request: Store-SCU to peer \"" + remote + "\""); + + if (context.GetScheduler().SubmitAndWait(job)) + { + // Success + call.GetOutput().AnswerBuffer("{}", "application/json"); + } + else + { + call.GetOutput().SignalError(HttpStatus_500_InternalServerError); + } } @@ -345,7 +387,7 @@ return peers.find(id) != peers.end(); } - static void ListPeers(RestApi::GetCall& call) + static void ListPeers(RestApiGetCall& call) { OrthancRestApi::SetOfStrings peers; Configuration::GetListOfOrthancPeers(peers); @@ -360,7 +402,7 @@ call.GetOutput().AnswerJson(result); } - static void ListPeerOperations(RestApi::GetCall& call) + static void ListPeerOperations(RestApiGetCall& call) { OrthancRestApi::SetOfStrings peers; Configuration::GetListOfOrthancPeers(peers); @@ -368,13 +410,11 @@ std::string id = call.GetUriComponent("id", ""); if (IsExistingPeer(peers, id)) { - Json::Value result = Json::arrayValue; - result.append("store"); - call.GetOutput().AnswerJson(result); + RestApi::AutoListChildren(call); } } - static void PeerStore(RestApi::PostCall& call) + static void PeerStore(RestApiPostCall& call) { ServerContext& context = OrthancRestApi::GetContext(call); @@ -389,35 +429,24 @@ OrthancPeerParameters peer; Configuration::GetOrthancPeer(peer, remote); - // Configure the HTTP client - HttpClient client; - if (peer.GetUsername().size() != 0 && - peer.GetPassword().size() != 0) - { - client.SetCredentials(peer.GetUsername().c_str(), - peer.GetPassword().c_str()); - } - - client.SetUrl(peer.GetUrl() + "instances"); - client.SetMethod(HttpMethod_Post); - - // Loop over the instances that are to be sent + ServerJob job; for (std::list<std::string>::const_iterator it = instances.begin(); it != instances.end(); ++it) { - LOG(INFO) << "Sending resource " << *it << " to peer \"" << remote << "\""; - - context.ReadFile(client.AccessPostData(), *it, FileContentType_Dicom); - - std::string answer; - if (!client.Apply(answer)) - { - LOG(ERROR) << "Unable to send resource " << *it << " to peer \"" << remote << "\""; - return; - } + job.AddCommand(new StorePeerCommand(context, peer, false)).AddInput(*it); } - call.GetOutput().AnswerBuffer("{}", "application/json"); + job.SetDescription("HTTP request: POST to peer \"" + remote + "\""); + + if (context.GetScheduler().SubmitAndWait(job)) + { + // Success + call.GetOutput().AnswerBuffer("{}", "application/json"); + } + else + { + call.GetOutput().SignalError(HttpStatus_500_InternalServerError); + } } @@ -429,7 +458,7 @@ return modalities.find(id) != modalities.end(); } - static void ListModalities(RestApi::GetCall& call) + static void ListModalities(RestApiGetCall& call) { OrthancRestApi::SetOfStrings modalities; Configuration::GetListOfDicomModalities(modalities); @@ -445,7 +474,7 @@ } - static void ListModalityOperations(RestApi::GetCall& call) + static void ListModalityOperations(RestApiGetCall& call) { OrthancRestApi::SetOfStrings modalities; Configuration::GetListOfDicomModalities(modalities); @@ -453,19 +482,12 @@ 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-instance"); - result.append("find"); - result.append("store"); - call.GetOutput().AnswerJson(result); + RestApi::AutoListChildren(call); } } - static void UpdateModality(RestApi::PutCall& call) + static void UpdateModality(RestApiPutCall& call) { Json::Value json; Json::Reader reader; @@ -479,14 +501,14 @@ } - static void DeleteModality(RestApi::DeleteCall& call) + static void DeleteModality(RestApiDeleteCall& call) { Configuration::RemoveModality(call.GetUriComponent("id", "")); call.GetOutput().AnswerBuffer("", "text/plain"); } - static void UpdatePeer(RestApi::PutCall& call) + static void UpdatePeer(RestApiPutCall& call) { Json::Value json; Json::Reader reader; @@ -500,7 +522,7 @@ } - static void DeletePeer(RestApi::DeleteCall& call) + static void DeletePeer(RestApiDeleteCall& call) { Configuration::RemovePeer(call.GetUriComponent("id", "")); call.GetOutput().AnswerBuffer("", "text/plain"); @@ -513,6 +535,7 @@ Register("/modalities/{id}", ListModalityOperations); Register("/modalities/{id}", UpdateModality); Register("/modalities/{id}", DeleteModality); + Register("/modalities/{id}/echo", DicomEcho); Register("/modalities/{id}/find-patient", DicomFindPatient); Register("/modalities/{id}/find-study", DicomFindStudy); Register("/modalities/{id}/find-series", DicomFindSeries);
--- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -35,6 +35,8 @@ #include "../ServerToolbox.h" #include "../FromDcmtkBridge.h" +#include "../ResourceFinder.h" +#include "../DicomFindQuery.h" #include <glog/logging.h> @@ -42,16 +44,48 @@ { // List all the patients, studies, series or instances ---------------------- - template <enum ResourceType resourceType> - static void ListResources(RestApi::GetCall& call) + static void AnswerListOfResources(RestApiOutput& output, + ServerIndex& index, + const std::list<std::string>& resources, + ResourceType level, + bool expand) { - Json::Value result; - OrthancRestApi::GetIndex(call).GetAllUuids(result, resourceType); - call.GetOutput().AnswerJson(result); + Json::Value answer = Json::arrayValue; + + for (std::list<std::string>::const_iterator + resource = resources.begin(); resource != resources.end(); resource++) + { + if (expand) + { + Json::Value item; + if (index.LookupResource(item, *resource, level)) + { + answer.append(item); + } + } + else + { + answer.append(*resource); + } + } + + output.AnswerJson(answer); + } + + + template <enum ResourceType resourceType> + static void ListResources(RestApiGetCall& call) + { + ServerIndex& index = OrthancRestApi::GetIndex(call); + + std::list<std::string> result; + index.GetAllUuids(result, resourceType); + + AnswerListOfResources(call.GetOutput(), index, result, resourceType, call.HasArgument("expand")); } template <enum ResourceType resourceType> - static void GetSingleResource(RestApi::GetCall& call) + static void GetSingleResource(RestApiGetCall& call) { Json::Value result; if (OrthancRestApi::GetIndex(call).LookupResource(result, call.GetUriComponent("id", ""), resourceType)) @@ -61,10 +95,10 @@ } template <enum ResourceType resourceType> - static void DeleteSingleResource(RestApi::DeleteCall& call) + static void DeleteSingleResource(RestApiDeleteCall& call) { Json::Value result; - if (OrthancRestApi::GetIndex(call).DeleteResource(result, call.GetUriComponent("id", ""), resourceType)) + if (OrthancRestApi::GetContext(call).DeleteResource(result, call.GetUriComponent("id", ""), resourceType)) { call.GetOutput().AnswerJson(result); } @@ -73,7 +107,7 @@ // Get information about a single patient ----------------------------------- - static void IsProtectedPatient(RestApi::GetCall& call) + static void IsProtectedPatient(RestApiGetCall& call) { std::string publicId = call.GetUriComponent("id", ""); bool isProtected = OrthancRestApi::GetIndex(call).IsProtectedPatient(publicId); @@ -81,7 +115,7 @@ } - static void SetPatientProtection(RestApi::PutCall& call) + static void SetPatientProtection(RestApiPutCall& call) { ServerContext& context = OrthancRestApi::GetContext(call); @@ -107,16 +141,16 @@ // Get information about a single instance ---------------------------------- - static void GetInstanceFile(RestApi::GetCall& call) + static void GetInstanceFile(RestApiGetCall& call) { ServerContext& context = OrthancRestApi::GetContext(call); std::string publicId = call.GetUriComponent("id", ""); - context.AnswerDicomFile(call.GetOutput(), publicId, FileContentType_Dicom); + context.AnswerAttachment(call.GetOutput(), publicId, FileContentType_Dicom); } - static void ExportInstanceFile(RestApi::PostCall& call) + static void ExportInstanceFile(RestApiPostCall& call) { ServerContext& context = OrthancRestApi::GetContext(call); @@ -132,29 +166,44 @@ template <bool simplify> - static void GetInstanceTags(RestApi::GetCall& call) + static void GetInstanceTags(RestApiGetCall& call) { ServerContext& context = OrthancRestApi::GetContext(call); std::string publicId = call.GetUriComponent("id", ""); - Json::Value full; - context.ReadJson(full, publicId); - if (simplify) { + Json::Value full; + context.ReadJson(full, publicId); + Json::Value simplified; SimplifyTags(simplified, full); call.GetOutput().AnswerJson(simplified); } else { - call.GetOutput().AnswerJson(full); + context.AnswerAttachment(call.GetOutput(), publicId, FileContentType_DicomAsJson); + } + } + + + static void GetInstanceTagsBis(RestApiGetCall& call) + { + bool simplify = call.HasArgument("simplify"); + + if (simplify) + { + GetInstanceTags<true>(call); + } + else + { + GetInstanceTags<false>(call); } } - static void ListFrames(RestApi::GetCall& call) + static void ListFrames(RestApiGetCall& call) { Json::Value instance; if (OrthancRestApi::GetIndex(call).LookupResource(instance, call.GetUriComponent("id", ""), ResourceType_Instance)) @@ -182,7 +231,7 @@ template <enum ImageExtractionMode mode> - static void GetImage(RestApi::GetCall& call) + static void GetImage(RestApiGetCall& call) { ServerContext& context = OrthancRestApi::GetContext(call); @@ -230,7 +279,7 @@ } - static void GetMatlabImage(RestApi::GetCall& call) + static void GetMatlabImage(RestApiGetCall& call) { ServerContext& context = OrthancRestApi::GetContext(call); @@ -264,7 +313,7 @@ - static void GetResourceStatistics(RestApi::GetCall& call) + static void GetResourceStatistics(RestApiGetCall& call) { std::string publicId = call.GetUriComponent("id", ""); Json::Value result; @@ -276,14 +325,14 @@ // Handling of metadata ----------------------------------------------------- - static void CheckValidResourceType(RestApi::Call& call) + static void CheckValidResourceType(RestApiCall& call) { std::string resourceType = call.GetUriComponent("resourceType", ""); StringToResourceType(resourceType.c_str()); } - static void ListMetadata(RestApi::GetCall& call) + static void ListMetadata(RestApiGetCall& call) { CheckValidResourceType(call); @@ -303,7 +352,7 @@ } - static void GetMetadata(RestApi::GetCall& call) + static void GetMetadata(RestApiGetCall& call) { CheckValidResourceType(call); @@ -319,7 +368,7 @@ } - static void DeleteMetadata(RestApi::DeleteCall& call) + static void DeleteMetadata(RestApiDeleteCall& call) { CheckValidResourceType(call); @@ -337,7 +386,7 @@ } - static void SetMetadata(RestApi::PutCall& call) + static void SetMetadata(RestApiPutCall& call) { CheckValidResourceType(call); @@ -360,7 +409,7 @@ // Handling of attached files ----------------------------------------------- - static void ListAttachments(RestApi::GetCall& call) + static void ListAttachments(RestApiGetCall& call) { std::string resourceType = call.GetUriComponent("resourceType", ""); std::string publicId = call.GetUriComponent("id", ""); @@ -379,7 +428,7 @@ } - static bool GetAttachmentInfo(FileInfo& info, RestApi::Call& call) + static bool GetAttachmentInfo(FileInfo& info, RestApiCall& call) { CheckValidResourceType(call); @@ -391,7 +440,7 @@ } - static void GetAttachmentOperations(RestApi::GetCall& call) + static void GetAttachmentOperations(RestApiGetCall& call) { FileInfo info; if (GetAttachmentInfo(info, call)) @@ -427,24 +476,30 @@ template <int uncompress> - static void GetAttachmentData(RestApi::GetCall& call) + static void GetAttachmentData(RestApiGetCall& call) { ServerContext& context = OrthancRestApi::GetContext(call); CheckValidResourceType(call); std::string publicId = call.GetUriComponent("id", ""); - std::string name = call.GetUriComponent("name", ""); + FileContentType type = StringToContentType(call.GetUriComponent("name", "")); - std::string content; - context.ReadFile(content, publicId, StringToContentType(name), - (uncompress == 1)); - - call.GetOutput().AnswerBuffer(content, "application/octet-stream"); + if (uncompress) + { + context.AnswerAttachment(call.GetOutput(), publicId, type); + } + else + { + // Return the raw data (possibly compressed), as stored on the filesystem + std::string content; + context.ReadFile(content, publicId, type, false); + call.GetOutput().AnswerBuffer(content, "application/octet-stream"); + } } - static void GetAttachmentSize(RestApi::GetCall& call) + static void GetAttachmentSize(RestApiGetCall& call) { FileInfo info; if (GetAttachmentInfo(info, call)) @@ -454,7 +509,7 @@ } - static void GetAttachmentCompressedSize(RestApi::GetCall& call) + static void GetAttachmentCompressedSize(RestApiGetCall& call) { FileInfo info; if (GetAttachmentInfo(info, call)) @@ -464,7 +519,7 @@ } - static void GetAttachmentMD5(RestApi::GetCall& call) + static void GetAttachmentMD5(RestApiGetCall& call) { FileInfo info; if (GetAttachmentInfo(info, call) && @@ -475,7 +530,7 @@ } - static void GetAttachmentCompressedMD5(RestApi::GetCall& call) + static void GetAttachmentCompressedMD5(RestApiGetCall& call) { FileInfo info; if (GetAttachmentInfo(info, call) && @@ -486,7 +541,7 @@ } - static void VerifyAttachment(RestApi::PostCall& call) + static void VerifyAttachment(RestApiPostCall& call) { ServerContext& context = OrthancRestApi::GetContext(call); CheckValidResourceType(call); @@ -540,7 +595,7 @@ } - static void UploadAttachment(RestApi::PutCall& call) + static void UploadAttachment(RestApiPutCall& call) { ServerContext& context = OrthancRestApi::GetContext(call); CheckValidResourceType(call); @@ -560,7 +615,7 @@ } - static void DeleteAttachment(RestApi::DeleteCall& call) + static void DeleteAttachment(RestApiDeleteCall& call) { CheckValidResourceType(call); @@ -580,7 +635,7 @@ // Raw access to the DICOM tags of an instance ------------------------------ - static void GetRawContent(RestApi::GetCall& call) + static void GetRawContent(RestApiGetCall& call) { std::string id = call.GetUriComponent("id", ""); @@ -591,6 +646,394 @@ + static bool ExtractSharedTags(Json::Value& shared, + ServerContext& context, + const std::string& publicId) + { + // Retrieve all the instances of this patient/study/series + typedef std::list<std::string> Instances; + Instances instances; + context.GetIndex().GetChildInstances(instances, publicId); // (*) + + // Loop over the instances + bool isFirst = true; + shared = Json::objectValue; + + for (Instances::const_iterator it = instances.begin(); + it != instances.end(); ++it) + { + // Get the tags of the current instance, in the simplified format + Json::Value tags; + + try + { + context.ReadJson(tags, *it); + } + catch (OrthancException&) + { + // Race condition: This instance has been removed since + // (*). Ignore this instance. + continue; + } + + if (tags.type() != Json::objectValue) + { + return false; // Error + } + + // Only keep the tags that are mapped to a string + Json::Value::Members members = tags.getMemberNames(); + for (size_t i = 0; i < members.size(); i++) + { + const Json::Value& tag = tags[members[i]]; + if (tag.type() != Json::objectValue || + tag["Type"].type() != Json::stringValue || + tag["Type"].asString() != "String") + { + tags.removeMember(members[i]); + } + } + + if (isFirst) + { + // This is the first instance, keep its tags as such + shared = tags; + isFirst = false; + } + else + { + // Loop over all the members of the shared tags extracted so + // far. If the value of one of these tags does not match its + // value in the current instance, remove it. + members = shared.getMemberNames(); + for (size_t i = 0; i < members.size(); i++) + { + if (!tags.isMember(members[i]) || + tags[members[i]]["Value"].asString() != shared[members[i]]["Value"].asString()) + { + shared.removeMember(members[i]); + } + } + } + } + + return true; + } + + + static void GetSharedTags(RestApiGetCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + std::string publicId = call.GetUriComponent("id", ""); + bool simplify = call.HasArgument("simplify"); + + Json::Value sharedTags; + if (ExtractSharedTags(sharedTags, context, publicId)) + { + // Success: Send the value of the shared tags + if (simplify) + { + Json::Value simplified; + SimplifyTags(simplified, sharedTags); + call.GetOutput().AnswerJson(simplified); + } + else + { + call.GetOutput().AnswerJson(sharedTags); + } + } + } + + + static void GetModuleInternal(RestApiGetCall& call, + ResourceType resourceType, + DicomModule module) + { + if (!((resourceType == ResourceType_Patient && module == DicomModule_Patient) || + (resourceType == ResourceType_Study && module == DicomModule_Patient) || + (resourceType == ResourceType_Study && module == DicomModule_Study) || + (resourceType == ResourceType_Series && module == DicomModule_Series) || + (resourceType == ResourceType_Instance && module == DicomModule_Instance) || + (resourceType == ResourceType_Instance && module == DicomModule_Image))) + { + throw OrthancException(ErrorCode_NotImplemented); + } + + ServerContext& context = OrthancRestApi::GetContext(call); + std::string publicId = call.GetUriComponent("id", ""); + bool simplify = call.HasArgument("simplify"); + + typedef std::set<DicomTag> ModuleTags; + ModuleTags moduleTags; + DicomTag::GetTagsForModule(moduleTags, module); + + Json::Value tags; + + if (resourceType != ResourceType_Instance) + { + // Retrieve all the instances of this patient/study/series + typedef std::list<std::string> Instances; + Instances instances; + context.GetIndex().GetChildInstances(instances, publicId); + + if (instances.empty()) + { + return; // Error: No instance (should never happen) + } + + // Select one child instance + publicId = instances.front(); + } + + context.ReadJson(tags, publicId); + + // Filter the tags of the instance according to the module + Json::Value result = Json::objectValue; + for (ModuleTags::const_iterator tag = moduleTags.begin(); tag != moduleTags.end(); ++tag) + { + std::string s = tag->Format(); + if (tags.isMember(s)) + { + result[s] = tags[s]; + } + } + + if (simplify) + { + Json::Value simplified; + SimplifyTags(simplified, result); + call.GetOutput().AnswerJson(simplified); + } + else + { + call.GetOutput().AnswerJson(result); + } + } + + + + template <enum ResourceType resourceType, + enum DicomModule module> + static void GetModule(RestApiGetCall& call) + { + GetModuleInternal(call, resourceType, module); + } + + + static void Lookup(RestApiPostCall& call) + { + typedef std::list< std::pair<ResourceType, std::string> > Resources; + + std::string tag = call.GetPostBody(); + Resources resources; + + OrthancRestApi::GetIndex(call).LookupIdentifier(resources, tag); + + Json::Value result = Json::arrayValue; + + for (Resources::const_iterator it = resources.begin(); + it != resources.end(); ++it) + { + ResourceType type = it->first; + const std::string& id = it->second; + + Json::Value item = Json::objectValue; + item["Type"] = EnumerationToString(type); + item["ID"] = id; + item["Path"] = GetBasePath(type, id); + + result.append(item); + } + + call.GetOutput().AnswerJson(result); + } + + + static void Find(RestApiPostCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + Json::Value request; + if (call.ParseJsonRequest(request) && + request.type() == Json::objectValue && + request.isMember("Level") && + request.isMember("Query") && + request["Level"].type() == Json::stringValue && + request["Query"].type() == Json::objectValue) + { + bool expand = false; + if (request.isMember("Expand")) + { + expand = request["Expand"].asBool(); + } + + std::string level = request["Level"].asString(); + + DicomFindQuery query; + query.SetLevel(StringToResourceType(level.c_str())); + + Json::Value::Members members = request["Query"].getMemberNames(); + for (size_t i = 0; i < members.size(); i++) + { + if (request["Query"][members[i]].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest); + } + + query.SetConstraint(FromDcmtkBridge::ParseTag(members[i]), + request["Query"][members[i]].asString()); + } + + std::list<std::string> resources; + ResourceFinder finder(context); + finder.Apply(resources, query); + AnswerListOfResources(call.GetOutput(), context.GetIndex(), resources, query.GetLevel(), expand); + } + else + { + throw OrthancException(ErrorCode_BadRequest); + } + } + + + template <enum ResourceType start, + enum ResourceType end> + static void GetChildResources(RestApiGetCall& call) + { + ServerIndex& index = OrthancRestApi::GetIndex(call); + + std::list<std::string> a, b, c; + a.push_back(call.GetUriComponent("id", "")); + + ResourceType type = start; + while (type != end) + { + b.clear(); + + for (std::list<std::string>::const_iterator + it = a.begin(); it != a.end(); ++it) + { + index.GetChildren(c, *it); + b.splice(b.begin(), c); + } + + switch (type) + { + case ResourceType_Patient: + type = ResourceType_Study; + break; + + case ResourceType_Study: + type = ResourceType_Series; + break; + + case ResourceType_Series: + type = ResourceType_Instance; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + a.clear(); + a.splice(a.begin(), b); + } + + Json::Value result = Json::arrayValue; + + for (std::list<std::string>::const_iterator + it = a.begin(); it != a.end(); ++it) + { + Json::Value item; + + if (OrthancRestApi::GetIndex(call).LookupResource(item, *it, end)) + { + result.append(item); + } + } + + call.GetOutput().AnswerJson(result); + } + + + static void GetChildInstancesTags(RestApiGetCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + std::string publicId = call.GetUriComponent("id", ""); + bool simplify = call.HasArgument("simplify"); + + // Retrieve all the instances of this patient/study/series + typedef std::list<std::string> Instances; + Instances instances; + + context.GetIndex().GetChildInstances(instances, publicId); // (*) + + Json::Value result = Json::objectValue; + + for (Instances::const_iterator it = instances.begin(); + it != instances.end(); ++it) + { + Json::Value full; + context.ReadJson(full, *it); + + if (simplify) + { + Json::Value simplified; + SimplifyTags(simplified, full); + result[*it] = simplified; + } + else + { + result[*it] = full; + } + } + + call.GetOutput().AnswerJson(result); + } + + + + template <enum ResourceType start, + enum ResourceType end> + static void GetParentResource(RestApiGetCall& call) + { + assert(start > end); + + ServerIndex& index = OrthancRestApi::GetIndex(call); + + std::string current = call.GetUriComponent("id", ""); + ResourceType currentType = start; + while (currentType > end) + { + std::string parent; + if (!index.LookupParent(parent, current)) + { + // Error that could happen if the resource gets deleted by + // another concurrent call + return; + } + + current = parent; + switch (currentType) + { + case ResourceType_Instance: currentType = ResourceType_Series; break; + case ResourceType_Series: currentType = ResourceType_Study; break; + case ResourceType_Study: currentType = ResourceType_Patient; break; + default: throw OrthancException(ErrorCode_InternalError); + } + } + + assert(currentType == end); + + Json::Value result; + if (index.LookupResource(result, current, end)) + { + call.GetOutput().AnswerJson(result); + } + } + + + void OrthancRestApi::RegisterResources() { Register("/instances", ListResources<ResourceType_Instance>); @@ -612,9 +1055,19 @@ Register("/studies/{id}/statistics", GetResourceStatistics); Register("/series/{id}/statistics", GetResourceStatistics); + Register("/patients/{id}/shared-tags", GetSharedTags); + Register("/series/{id}/shared-tags", GetSharedTags); + Register("/studies/{id}/shared-tags", GetSharedTags); + + Register("/instances/{id}/module", GetModule<ResourceType_Instance, DicomModule_Instance>); + Register("/patients/{id}/module", GetModule<ResourceType_Patient, DicomModule_Patient>); + Register("/series/{id}/module", GetModule<ResourceType_Series, DicomModule_Series>); + Register("/studies/{id}/module", GetModule<ResourceType_Study, DicomModule_Study>); + Register("/studies/{id}/module-patient", GetModule<ResourceType_Study, DicomModule_Patient>); + Register("/instances/{id}/file", GetInstanceFile); Register("/instances/{id}/export", ExportInstanceFile); - Register("/instances/{id}/tags", GetInstanceTags<false>); + Register("/instances/{id}/tags", GetInstanceTagsBis); Register("/instances/{id}/simplified-tags", GetInstanceTags<true>); Register("/instances/{id}/frames", ListFrames); @@ -649,6 +1102,27 @@ Register("/{resourceType}/{id}/attachments/{name}/verify-md5", VerifyAttachment); Register("/{resourceType}/{id}/attachments/{name}", UploadAttachment); + Register("/tools/lookup", Lookup); + Register("/tools/find", Find); + + Register("/patients/{id}/studies", GetChildResources<ResourceType_Patient, ResourceType_Study>); + Register("/patients/{id}/series", GetChildResources<ResourceType_Patient, ResourceType_Series>); + Register("/patients/{id}/instances", GetChildResources<ResourceType_Patient, ResourceType_Instance>); + Register("/studies/{id}/series", GetChildResources<ResourceType_Study, ResourceType_Series>); + Register("/studies/{id}/instances", GetChildResources<ResourceType_Study, ResourceType_Instance>); + Register("/series/{id}/instances", GetChildResources<ResourceType_Series, ResourceType_Instance>); + + Register("/studies/{id}/patient", GetParentResource<ResourceType_Study, ResourceType_Patient>); + Register("/series/{id}/patient", GetParentResource<ResourceType_Series, ResourceType_Patient>); + Register("/series/{id}/study", GetParentResource<ResourceType_Series, ResourceType_Study>); + Register("/instances/{id}/patient", GetParentResource<ResourceType_Instance, ResourceType_Patient>); + Register("/instances/{id}/study", GetParentResource<ResourceType_Instance, ResourceType_Study>); + Register("/instances/{id}/series", GetParentResource<ResourceType_Instance, ResourceType_Series>); + + Register("/patients/{id}/instances-tags", GetChildInstancesTags); + Register("/studies/{id}/instances-tags", GetChildInstancesTags); + Register("/series/{id}/instances-tags", GetChildInstancesTags); + Register("/instances/{id}/content/*", GetRawContent); } }
--- a/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -35,6 +35,8 @@ #include "../OrthancInitialization.h" #include "../FromDcmtkBridge.h" +#include "../../Plugins/Engine/PluginsManager.h" +#include "../../Plugins/Engine/OrthancPlugins.h" #include <glog/logging.h> @@ -43,29 +45,30 @@ { // System information ------------------------------------------------------- - static void ServeRoot(RestApi::GetCall& call) + static void ServeRoot(RestApiGetCall& call) { call.GetOutput().Redirect("app/explorer.html"); } - static void GetSystemInformation(RestApi::GetCall& call) + static void GetSystemInformation(RestApiGetCall& call) { Json::Value result = Json::objectValue; result["Version"] = ORTHANC_VERSION; result["Name"] = Configuration::GetGlobalStringParameter("Name", ""); + result["DatabaseVersion"] = boost::lexical_cast<int>(OrthancRestApi::GetIndex(call).GetGlobalProperty(GlobalProperty_DatabaseSchemaVersion, "0")); call.GetOutput().AnswerJson(result); } - static void GetStatistics(RestApi::GetCall& call) + static void GetStatistics(RestApiGetCall& call) { Json::Value result = Json::objectValue; OrthancRestApi::GetIndex(call).ComputeStatistics(result); call.GetOutput().AnswerJson(result); } - static void GenerateUid(RestApi::GetCall& call) + static void GenerateUid(RestApiGetCall& call) { std::string level = call.GetArgument("level", ""); if (level == "patient") @@ -86,19 +89,122 @@ } } - static void ExecuteScript(RestApi::PostCall& call) + static void ExecuteScript(RestApiPostCall& call) { std::string result; ServerContext& context = OrthancRestApi::GetContext(call); - context.GetLuaContext().Execute(result, call.GetPostBody()); + + { + ServerContext::LuaContextLocker locker(context); + locker.GetLua().Execute(result, call.GetPostBody()); + } + call.GetOutput().AnswerBuffer(result, "text/plain"); } - static void GetNowIsoString(RestApi::GetCall& call) + static void GetNowIsoString(RestApiGetCall& call) { call.GetOutput().AnswerBuffer(Toolbox::GetNowIsoString(), "text/plain"); } + + static void GetDicomConformanceStatement(RestApiGetCall& call) + { + std::string statement; + GetFileResource(statement, EmbeddedResources::DICOM_CONFORMANCE_STATEMENT); + call.GetOutput().AnswerBuffer(statement, "text/plain"); + } + + + // Plugins information ------------------------------------------------------ + + static void ListPlugins(RestApiGetCall& call) + { + Json::Value v = Json::arrayValue; + + v.append("explorer.js"); + + if (OrthancRestApi::GetContext(call).HasPlugins()) + { + std::list<std::string> plugins; + OrthancRestApi::GetContext(call).GetPluginsManager().ListPlugins(plugins); + + for (std::list<std::string>::const_iterator + it = plugins.begin(); it != plugins.end(); ++it) + { + v.append(*it); + } + } + + call.GetOutput().AnswerJson(v); + } + + + static void GetPlugin(RestApiGetCall& call) + { + if (!OrthancRestApi::GetContext(call).HasPlugins()) + { + return; + } + + const PluginsManager& manager = OrthancRestApi::GetContext(call).GetPluginsManager(); + std::string id = call.GetUriComponent("id", ""); + + if (manager.HasPlugin(id)) + { + Json::Value v = Json::objectValue; + v["ID"] = id; + v["Version"] = manager.GetPluginVersion(id); + + const OrthancPlugins& plugins = OrthancRestApi::GetContext(call).GetOrthancPlugins(); + const char *c = plugins.GetProperty(id.c_str(), _OrthancPluginProperty_RootUri); + if (c != NULL) + { + v["RootUri"] = c; + } + + c = plugins.GetProperty(id.c_str(), _OrthancPluginProperty_Description); + if (c != NULL) + { + v["Description"] = c; + } + + c = plugins.GetProperty(id.c_str(), _OrthancPluginProperty_OrthancExplorer); + v["ExtendsOrthancExplorer"] = (c != NULL); + + call.GetOutput().AnswerJson(v); + } + } + + + static void GetOrthancExplorerPlugins(RestApiGetCall& call) + { + std::string s = "// Extensions to Orthanc Explorer by the registered plugins\n\n"; + + if (OrthancRestApi::GetContext(call).HasPlugins()) + { + const PluginsManager& manager = OrthancRestApi::GetContext(call).GetPluginsManager(); + const OrthancPlugins& plugins = OrthancRestApi::GetContext(call).GetOrthancPlugins(); + + std::list<std::string> lst; + OrthancRestApi::GetContext(call).GetPluginsManager().ListPlugins(lst); + + for (std::list<std::string>::const_iterator + it = lst.begin(); it != lst.end(); ++it) + { + const char* tmp = plugins.GetProperty(it->c_str(), _OrthancPluginProperty_OrthancExplorer); + if (tmp != NULL) + { + s += "/**\n * From plugin: " + *it + " (version " + manager.GetPluginVersion(*it) + ")\n **/\n\n"; + s += std::string(tmp) + "\n\n"; + } + } + } + + call.GetOutput().AnswerBuffer(s, "application/javascript"); + } + + void OrthancRestApi::RegisterSystem() { Register("/", ServeRoot); @@ -107,5 +213,10 @@ Register("/tools/generate-uid", GenerateUid); Register("/tools/execute-script", ExecuteScript); Register("/tools/now", GetNowIsoString); + Register("/tools/dicom-conformance", GetDicomConformanceStatement); + + Register("/plugins", ListPlugins); + Register("/plugins/{id}", GetPlugin); + Register("/plugins/explorer.js", GetOrthancExplorerPlugins); } }
--- a/OrthancServer/ParsedDicomFile.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/ParsedDicomFile.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -80,6 +80,7 @@ #include "ParsedDicomFile.h" +#include "ServerToolbox.h" #include "FromDcmtkBridge.h" #include "ToDcmtkBridge.h" #include "Internals/DicomImageDecoder.h" @@ -146,6 +147,7 @@ struct ParsedDicomFile::PImpl { std::auto_ptr<DcmFileFormat> file_; + Encoding encoding_; }; @@ -170,6 +172,8 @@ } pimpl_->file_->loadAllDataIntoMemory(); pimpl_->file_->transferEnd(); + + pimpl_->encoding_ = FromDcmtkBridge::DetectEncoding(*pimpl_->file_->getDataset()); } @@ -261,7 +265,8 @@ Uint32 length = element.getLength(transferSyntax); Uint32 offset = 0; - output.GetLowLevelOutput().SendOkHeader(CONTENT_TYPE_OCTET_STREAM, true, length, NULL); + output.GetLowLevelOutput().SetContentType(CONTENT_TYPE_OCTET_STREAM); + output.GetLowLevelOutput().SetContentLength(length); while (offset < length) { @@ -279,7 +284,7 @@ if (cond.good()) { - output.GetLowLevelOutput().Send(&buffer[0], nbytes); + output.GetLowLevelOutput().SendBody(&buffer[0], nbytes); offset += nbytes; } else @@ -753,24 +758,42 @@ - void ParsedDicomFile::RemovePrivateTags() + void ParsedDicomFile::RemovePrivateTagsInternal(const std::set<DicomTag>* toKeep) { + DcmDataset& dataset = *pimpl_->file_->getDataset(); + + // Loop over the dataset to detect its private tags typedef std::list<DcmElement*> Tags; - Tags privateTags; - DcmDataset& dataset = *pimpl_->file_->getDataset(); for (unsigned long i = 0; i < dataset.card(); i++) { DcmElement* element = dataset.getElement(i); DcmTag tag(element->getTag()); - if (!strcmp("PrivateCreator", tag.getTagName()) || // TODO - This may change with future versions of DCMTK - tag.getPrivateCreator() != NULL) + + // Is this a private tag? + if (FromDcmtkBridge::IsPrivateTag(tag)) { - privateTags.push_back(element); + bool remove = true; + + // Check whether this private tag is to be kept + if (toKeep != NULL) + { + DicomTag tmp = FromDcmtkBridge::Convert(tag); + if (toKeep->find(tmp) != toKeep->end()) + { + remove = false; // Keep it + } + } + + if (remove) + { + privateTags.push_back(element); + } } } + // Loop over the detected private tags to remove them for (Tags::iterator it = privateTags.begin(); it != privateTags.end(); ++it) { @@ -784,13 +807,30 @@ + void ParsedDicomFile::Insert(const DicomTag& tag, const std::string& value) { - std::auto_ptr<DcmElement> element(CreateElementForTag(tag)); - FillElementWithString(*element, tag, value); + OFCondition cond; + + if (FromDcmtkBridge::IsPrivateTag(tag)) + { + // This is a private tag + // http://support.dcmtk.org/redmine/projects/dcmtk/wiki/howto_addprivatedata - if (!pimpl_->file_->getDataset()->insert(element.release(), false, false).good()) + DcmTag key(tag.GetGroup(), tag.GetElement(), EVR_OB); + cond = pimpl_->file_->getDataset()->putAndInsertUint8Array + (key, (const Uint8*) value.c_str(), value.size(), false); + } + else + { + std::auto_ptr<DcmElement> element(CreateElementForTag(tag)); + FillElementWithString(*element, tag, value); + + cond = pimpl_->file_->getDataset()->insert(element.release(), false, false); + } + + if (!cond.good()) { // This field already exists throw OrthancException(ErrorCode_InternalError); @@ -824,7 +864,17 @@ } else { - FillElementWithString(*element, tag, value); + if (FromDcmtkBridge::IsPrivateTag(tag)) + { + if (!element->putUint8Array((const Uint8*) value.c_str(), value.size()).good()) + { + throw OrthancException(ErrorCode_InternalError); + } + } + else + { + FillElementWithString(*element, tag, value); + } } @@ -852,7 +902,7 @@ void ParsedDicomFile::Answer(RestApiOutput& output) { std::string serialized; - if (FromDcmtkBridge::SaveToMemoryBuffer(serialized, pimpl_->file_->getDataset())) + if (FromDcmtkBridge::SaveToMemoryBuffer(serialized, *pimpl_->file_->getDataset())) { output.AnswerBuffer(serialized, CONTENT_TYPE_OCTET_STREAM); } @@ -865,29 +915,56 @@ { DcmTagKey k(tag.GetGroup(), tag.GetElement()); DcmDataset& dataset = *pimpl_->file_->getDataset(); - DcmElement* element = NULL; - if (!dataset.findAndGetElement(k, element).good() || - element == NULL) + + if (FromDcmtkBridge::IsPrivateTag(tag)) { - return false; - } + const Uint8* data = NULL; // This is freed in the destructor of the dataset + long unsigned int count = 0; - std::auto_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement(*element)); + if (dataset.findAndGetUint8Array(k, data, &count).good()) + { + if (count > 0) + { + assert(data != NULL); + value.assign(reinterpret_cast<const char*>(data), count); + } + else + { + value.clear(); + } - if (v.get() == NULL) - { - value = ""; + return true; + } + else + { + return false; + } } else { - value = v->AsString(); - } + DcmElement* element = NULL; + if (!dataset.findAndGetElement(k, element).good() || + element == NULL) + { + return false; + } + + std::auto_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement(*element, pimpl_->encoding_)); - return true; + if (v.get() == NULL) + { + value = ""; + } + else + { + value = v->AsString(); + } + + return true; + } } - DicomInstanceHasher ParsedDicomFile::GetHasher() { std::string patientId, studyUid, seriesUid, instanceUid; @@ -904,98 +981,6 @@ } - static void StoreElement(Json::Value& target, - DcmElement& element, - unsigned int maxStringLength); - - static void StoreItem(Json::Value& target, - DcmItem& item, - unsigned int maxStringLength) - { - target = Json::Value(Json::objectValue); - - for (unsigned long i = 0; i < item.card(); i++) - { - DcmElement* element = item.getElement(i); - StoreElement(target, *element, maxStringLength); - } - } - - - static void StoreElement(Json::Value& target, - DcmElement& element, - unsigned int maxStringLength) - { - assert(target.type() == Json::objectValue); - - DicomTag tag(FromDcmtkBridge::GetTag(element)); - const std::string formattedTag = tag.Format(); - -#if 0 - const std::string tagName = FromDcmtkBridge::GetName(tag); -#else - // This version of the code gives access to the name of the private tags - DcmTag tagbis(element.getTag()); - const std::string tagName(tagbis.getTagName()); -#endif - - if (element.isLeaf()) - { - Json::Value value(Json::objectValue); - value["Name"] = tagName; - - if (tagbis.getPrivateCreator() != NULL) - { - value["PrivateCreator"] = tagbis.getPrivateCreator(); - } - - std::auto_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement(element)); - if (v->IsNull()) - { - value["Type"] = "Null"; - value["Value"] = Json::nullValue; - } - else - { - std::string s = v->AsString(); - if (maxStringLength == 0 || - s.size() <= maxStringLength) - { - value["Type"] = "String"; - value["Value"] = s; - } - else - { - value["Type"] = "TooLong"; - value["Value"] = Json::nullValue; - } - } - - target[formattedTag] = value; - } - else - { - Json::Value children(Json::arrayValue); - - // "All subclasses of DcmElement except for DcmSequenceOfItems - // are leaf nodes, while DcmSequenceOfItems, DcmItem, DcmDataset - // etc. are not." The following cast is thus OK. - DcmSequenceOfItems& sequence = dynamic_cast<DcmSequenceOfItems&>(element); - - for (unsigned long i = 0; i < sequence.card(); i++) - { - DcmItem* child = sequence.getItem(i); - Json::Value& v = children.append(Json::objectValue); - StoreItem(v, *child, maxStringLength); - } - - target[formattedTag]["Name"] = tagName; - target[formattedTag]["Type"] = "Sequence"; - target[formattedTag]["Value"] = children; - } - } - - template <typename T> static void ExtractPngImageTruncate(std::string& result, DicomIntegerPixelAccessor& accessor, @@ -1028,7 +1013,7 @@ void ParsedDicomFile::SaveToMemoryBuffer(std::string& buffer) { - FromDcmtkBridge::SaveToMemoryBuffer(buffer, pimpl_->file_->getDataset()); + FromDcmtkBridge::SaveToMemoryBuffer(buffer, *pimpl_->file_->getDataset()); } @@ -1044,6 +1029,7 @@ ParsedDicomFile::ParsedDicomFile() : pimpl_(new PImpl) { pimpl_->file_.reset(new DcmFileFormat); + pimpl_->encoding_ = Encoding_Ascii; Replace(DICOM_TAG_PATIENT_ID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Patient)); Replace(DICOM_TAG_STUDY_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study)); Replace(DICOM_TAG_SERIES_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Series)); @@ -1073,6 +1059,8 @@ pimpl_(new PImpl) { pimpl_->file_.reset(dynamic_cast<DcmFileFormat*>(other.pimpl_->file_->clone())); + + pimpl_->encoding_ = other.pimpl_->encoding_; } @@ -1241,15 +1229,15 @@ switch (mode) { case ImageExtractionMode_UInt8: - ok = DicomImageDecoder::DecodeAndTruncate(result, dataset, frame, PixelFormat_Grayscale8); + ok = DicomImageDecoder::DecodeAndTruncate(result, dataset, frame, PixelFormat_Grayscale8, false); break; case ImageExtractionMode_UInt16: - ok = DicomImageDecoder::DecodeAndTruncate(result, dataset, frame, PixelFormat_Grayscale16); + ok = DicomImageDecoder::DecodeAndTruncate(result, dataset, frame, PixelFormat_Grayscale16, false); break; case ImageExtractionMode_Int16: - ok = DicomImageDecoder::DecodeAndTruncate(result, dataset, frame, PixelFormat_SignedGrayscale16); + ok = DicomImageDecoder::DecodeAndTruncate(result, dataset, frame, PixelFormat_SignedGrayscale16, false); break; case ImageExtractionMode_Preview: @@ -1279,4 +1267,96 @@ writer.WriteToMemory(result, accessor); } + + Encoding ParsedDicomFile::GetEncoding() const + { + return pimpl_->encoding_; + } + + + void ParsedDicomFile::SetEncoding(Encoding encoding) + { + std::string s; + + // http://www.dabsoft.ch/dicom/3/C.12.1.1.2/ + switch (encoding) + { + case Encoding_Utf8: + case Encoding_Ascii: + s = "ISO_IR 192"; + break; + + case Encoding_Windows1251: + // This Cyrillic codepage is not officially supported by the + // DICOM standard. Do not set the SpecificCharacterSet tag. + return; + + case Encoding_Latin1: + s = "ISO_IR 100"; + break; + + case Encoding_Latin2: + s = "ISO_IR 101"; + break; + + case Encoding_Latin3: + s = "ISO_IR 109"; + break; + + case Encoding_Latin4: + s = "ISO_IR 110"; + break; + + case Encoding_Latin5: + s = "ISO_IR 148"; + break; + + case Encoding_Cyrillic: + s = "ISO_IR 144"; + break; + + case Encoding_Arabic: + s = "ISO_IR 127"; + break; + + case Encoding_Greek: + s = "ISO_IR 126"; + break; + + case Encoding_Hebrew: + s = "ISO_IR 138"; + break; + + case Encoding_Japanese: + s = "ISO_IR 13"; + break; + + case Encoding_Chinese: + s = "GB18030"; + break; + + case Encoding_Thai: + s = "ISO_IR 166"; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + Replace(DICOM_TAG_SPECIFIC_CHARACTER_SET, s, DicomReplaceMode_InsertIfAbsent); + } + + void ParsedDicomFile::ToJson(Json::Value& target, bool simplify) + { + if (simplify) + { + Json::Value tmp; + FromDcmtkBridge::ToJson(tmp, *pimpl_->file_->getDataset()); + SimplifyTags(target, tmp); + } + else + { + FromDcmtkBridge::ToJson(target, *pimpl_->file_->getDataset()); + } + } }
--- a/OrthancServer/ParsedDicomFile.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/ParsedDicomFile.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -51,6 +51,8 @@ void Setup(const char* content, size_t size); + void RemovePrivateTagsInternal(const std::set<DicomTag>* toKeep); + public: ParsedDicomFile(); // Create a minimal DICOM instance @@ -79,7 +81,15 @@ const std::string& value, DicomReplaceMode mode = DicomReplaceMode_InsertIfAbsent); - void RemovePrivateTags(); + void RemovePrivateTags() + { + RemovePrivateTagsInternal(NULL); + } + + void RemovePrivateTags(const std::set<DicomTag>& toKeep) + { + RemovePrivateTagsInternal(&toKeep); + } bool GetTagValue(std::string& value, const DicomTag& tag); @@ -104,6 +114,13 @@ void ExtractPngImage(std::string& result, unsigned int frame, ImageExtractionMode mode); + + Encoding GetEncoding() const; + + void SetEncoding(Encoding encoding); + + void ToJson(Json::Value& target, + bool simplify); }; }
--- a/OrthancServer/PrecompiledHeadersServer.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/PrecompiledHeadersServer.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancServer/PrecompiledHeadersServer.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/PrecompiledHeadersServer.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancServer/PrepareDatabase.sql Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/PrepareDatabase.sql Thu May 21 16:58:30 2015 +0200 @@ -18,6 +18,15 @@ PRIMARY KEY(id, tagGroup, tagElement) ); +-- The following table was added in Orthanc 0.8.5 (database v5) +CREATE TABLE DicomIdentifiers( + id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, + tagGroup INTEGER, + tagElement INTEGER, + value TEXT, + PRIMARY KEY(id, tagGroup, tagElement) + ); + CREATE TABLE Metadata( id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, type INTEGER, @@ -68,8 +77,14 @@ CREATE INDEX PatientRecyclingIndex ON PatientRecyclingOrder(patientId); CREATE INDEX MainDicomTagsIndex1 ON MainDicomTags(id); -CREATE INDEX MainDicomTagsIndex2 ON MainDicomTags(tagGroup, tagElement); -CREATE INDEX MainDicomTagsIndexValues ON MainDicomTags(value COLLATE BINARY); +-- The 2 following indexes were removed in Orthanc 0.8.5 (database v5), to speed up +-- CREATE INDEX MainDicomTagsIndex2 ON MainDicomTags(tagGroup, tagElement); +-- CREATE INDEX MainDicomTagsIndexValues ON MainDicomTags(value COLLATE BINARY); + +-- The 3 following indexes were added in Orthanc 0.8.5 (database v5) +CREATE INDEX DicomIdentifiersIndex1 ON DicomIdentifiers(id); +CREATE INDEX DicomIdentifiersIndex2 ON DicomIdentifiers(tagGroup, tagElement); +CREATE INDEX DicomIdentifiersIndexValues ON DicomIdentifiers(value COLLATE BINARY); CREATE INDEX ChangesIndex ON Changes(internalId); @@ -85,6 +100,7 @@ CREATE TRIGGER ResourceDeleted AFTER DELETE ON Resources BEGIN + SELECT SignalResourceDeleted(old.publicId, old.resourceType); -- New in Orthanc 0.8.5 (db v5) SELECT SignalRemainingAncestor(parent.publicId, parent.resourceType) FROM Resources AS parent WHERE internalId = old.parentId; END; @@ -107,4 +123,4 @@ -- Set the version of the database schema -- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration -INSERT INTO GlobalProperties VALUES (1, "4"); +INSERT INTO GlobalProperties VALUES (1, "5");
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ResourceFinder.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,383 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "PrecompiledHeadersServer.h" +#include "ResourceFinder.h" + +#include "FromDcmtkBridge.h" +#include "ServerContext.h" + +#include <glog/logging.h> +#include <boost/algorithm/string/predicate.hpp> + +namespace Orthanc +{ + class ResourceFinder::CandidateResources + { + private: + typedef std::map<DicomTag, std::string> Query; + + ResourceFinder& finder_; + ServerIndex& index_; + ResourceType level_; + bool isFilterApplied_; + std::set<std::string> filtered_; + + + static void ListToSet(std::set<std::string>& target, + const std::list<std::string>& source) + { + for (std::list<std::string>::const_iterator + it = source.begin(); it != source.end(); ++it) + { + target.insert(*it); + } + } + + + public: + CandidateResources(ResourceFinder& finder) : + finder_(finder), + index_(finder.context_.GetIndex()), + level_(ResourceType_Patient), + isFilterApplied_(false) + { + } + + ResourceType GetLevel() const + { + return level_; + } + + void GoDown() + { + assert(level_ != ResourceType_Instance); + + if (isFilterApplied_) + { + std::set<std::string> tmp = filtered_; + + filtered_.clear(); + + for (std::set<std::string>::const_iterator + it = tmp.begin(); it != tmp.end(); ++it) + { + std::list<std::string> children; + try + { + index_.GetChildren(children, *it); + ListToSet(filtered_, children); + } + catch (OrthancException&) + { + // The resource was removed in the meantime + } + } + } + + switch (level_) + { + case ResourceType_Patient: + level_ = ResourceType_Study; + break; + + case ResourceType_Study: + level_ = ResourceType_Series; + break; + + case ResourceType_Series: + level_ = ResourceType_Instance; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + void Flatten(std::list<std::string>& resources) const + { + resources.clear(); + + if (isFilterApplied_) + { + for (std::set<std::string>::const_iterator + it = filtered_.begin(); it != filtered_.end(); ++it) + { + resources.push_back(*it); + } + } + else + { + index_.GetAllUuids(resources, level_); + } + } + + + void RestrictIdentifier(const IQuery& query, + const DicomTag& tag) + { + assert((level_ == ResourceType_Patient && tag == DICOM_TAG_PATIENT_ID) || + (level_ == ResourceType_Study && tag == DICOM_TAG_STUDY_INSTANCE_UID) || + (level_ == ResourceType_Study && tag == DICOM_TAG_ACCESSION_NUMBER) || + (level_ == ResourceType_Series && tag == DICOM_TAG_SERIES_INSTANCE_UID) || + (level_ == ResourceType_Instance && tag == DICOM_TAG_SOP_INSTANCE_UID)); + + std::string value; + if (!query.RestrictIdentifier(value, tag)) + { + return; + } + + LOG(INFO) << "Lookup for identifier tag " + << FromDcmtkBridge::GetName(tag) << " (value: " << value << ")"; + + std::list<std::string> resources; + index_.LookupIdentifier(resources, tag, value, level_); + + if (isFilterApplied_) + { + std::set<std::string> s; + ListToSet(s, resources); + + std::set<std::string> tmp = filtered_; + filtered_.clear(); + + for (std::set<std::string>::const_iterator + it = tmp.begin(); it != tmp.end(); ++it) + { + if (s.find(*it) != s.end()) + { + filtered_.insert(*it); + } + } + } + else + { + assert(filtered_.empty()); + isFilterApplied_ = true; + ListToSet(filtered_, resources); + } + } + + + void RestrictMainDicomTags(const IQuery& query) + { + if (!query.HasMainDicomTagsFilter(level_)) + { + return; + } + + std::list<std::string> resources; + Flatten(resources); + + isFilterApplied_ = true; + filtered_.clear(); + + for (std::list<std::string>::const_iterator + it = resources.begin(); it != resources.end(); it++) + { + DicomMap mainTags; + if (index_.GetMainDicomTags(mainTags, *it, level_)) + { + if (query.FilterMainDicomTags(*it, level_, mainTags)) + { + filtered_.insert(*it); + } + } + } + } + }; + + + ResourceFinder::ResourceFinder(ServerContext& context) : + context_(context), + maxResults_(0) + { + } + + + void ResourceFinder::ApplyAtLevel(CandidateResources& candidates, + const IQuery& query, + ResourceType level) + { + if (level != ResourceType_Patient) + { + candidates.GoDown(); + } + + switch (level) + { + case ResourceType_Patient: + { + candidates.RestrictIdentifier(query, DICOM_TAG_PATIENT_ID); + break; + } + + case ResourceType_Study: + { + candidates.RestrictIdentifier(query, DICOM_TAG_STUDY_INSTANCE_UID); + candidates.RestrictIdentifier(query, DICOM_TAG_ACCESSION_NUMBER); + break; + } + + case ResourceType_Series: + { + candidates.RestrictIdentifier(query, DICOM_TAG_SERIES_INSTANCE_UID); + break; + } + + case ResourceType_Instance: + { + candidates.RestrictIdentifier(query, DICOM_TAG_SOP_INSTANCE_UID); + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + + candidates.RestrictMainDicomTags(query); + } + + + + static bool LookupOneInstance(std::string& result, + ServerIndex& index, + const std::string& id, + ResourceType type) + { + if (type == ResourceType_Instance) + { + result = id; + return true; + } + + std::string childId; + + { + std::list<std::string> children; + index.GetChildInstances(children, id); + + if (children.empty()) + { + return false; + } + + childId = children.front(); + } + + return LookupOneInstance(result, index, childId, GetChildResourceType(type)); + } + + + bool ResourceFinder::Apply(std::list<std::string>& result, + const IQuery& query) + { + CandidateResources candidates(*this); + + ApplyAtLevel(candidates, query, ResourceType_Patient); + + const ResourceType level = query.GetLevel(); + + if (level == ResourceType_Study || + level == ResourceType_Series || + level == ResourceType_Instance) + { + ApplyAtLevel(candidates, query, ResourceType_Study); + } + + if (level == ResourceType_Series || + level == ResourceType_Instance) + { + ApplyAtLevel(candidates, query, ResourceType_Series); + } + + if (level == ResourceType_Instance) + { + ApplyAtLevel(candidates, query, ResourceType_Instance); + } + + if (!query.HasInstanceFilter()) + { + candidates.Flatten(result); + + if (maxResults_ != 0 && + result.size() >= maxResults_) + { + result.resize(maxResults_); + return false; + } + else + { + return true; + } + } + else + { + std::list<std::string> tmp; + candidates.Flatten(tmp); + + result.clear(); + for (std::list<std::string>::const_iterator + resource = tmp.begin(); resource != tmp.end(); ++resource) + { + try + { + std::string instance; + if (LookupOneInstance(instance, context_.GetIndex(), *resource, level)) + { + Json::Value content; + context_.ReadJson(content, instance); + if (query.FilterInstance(*resource, content)) + { + result.push_back(*resource); + + if (maxResults_ != 0 && + result.size() >= maxResults_) + { + // Too many results, stop before recording this new match + return false; + } + } + } + } + catch (OrthancException&) + { + // This resource has been deleted since the search was started + } + } + } + + return true; // All the matching resources have been returned + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ResourceFinder.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,101 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "ServerIndex.h" + +#include <boost/noncopyable.hpp> + +namespace Orthanc +{ + class ResourceFinder : public boost::noncopyable + { + public: + class IQuery : public boost::noncopyable + { + public: + virtual ~IQuery() + { + } + + virtual ResourceType GetLevel() const = 0; + + virtual bool RestrictIdentifier(std::string& value, + DicomTag identifier) const = 0; + + virtual bool HasMainDicomTagsFilter(ResourceType level) const = 0; + + virtual bool FilterMainDicomTags(const std::string& resourceId, + ResourceType level, + const DicomMap& mainTags) const = 0; + + virtual bool HasInstanceFilter() const = 0; + + virtual bool FilterInstance(const std::string& instanceId, + const Json::Value& content) const = 0; + }; + + + private: + typedef std::map<DicomTag, std::string> Identifiers; + + class CandidateResources; + + ServerContext& context_; + size_t maxResults_; + + void ApplyAtLevel(CandidateResources& candidates, + const IQuery& query, + ResourceType level); + + public: + ResourceFinder(ServerContext& context); + + void SetMaxResults(size_t value) + { + maxResults_ = value; + } + + size_t GetMaxResults() const + { + return maxResults_; + } + + // Returns "true" iff. all the matching resources have been + // returned. Will be "false" if the results were truncated by + // "SetMaxResults()". + bool Apply(std::list<std::string>& result, + const IQuery& query); + }; + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/CallSystemCommand.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,83 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "CallSystemCommand.h" + +#include <glog/logging.h> +#include "../../Core/Toolbox.h" +#include "../../Core/Uuid.h" + +namespace Orthanc +{ + CallSystemCommand::CallSystemCommand(ServerContext& context, + const std::string& command, + const std::vector<std::string>& arguments) : + context_(context), + command_(command), + arguments_(arguments) + { + } + + bool CallSystemCommand::Apply(ListOfStrings& outputs, + const ListOfStrings& inputs) + { + for (ListOfStrings::const_iterator + it = inputs.begin(); it != inputs.end(); ++it) + { + LOG(INFO) << "Calling system command " << command_ << " on instance " << *it; + + try + { + std::string dicom; + context_.ReadFile(dicom, *it, FileContentType_Dicom); + + Toolbox::TemporaryFile tmp; + tmp.Write(dicom); + + std::vector<std::string> args = arguments_; + args.push_back(tmp.GetPath()); + + Toolbox::ExecuteSystemCommand(command_, args); + + // Only chain with other commands if this command succeeds + outputs.push_back(*it); + } + catch (OrthancException& e) + { + LOG(ERROR) << "Unable to call system command " << command_ + << " on instance " << *it << " in a Lua script: " << e.What(); + } + } + + return true; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/CallSystemCommand.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,55 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "IServerCommand.h" +#include "../ServerContext.h" + +namespace Orthanc +{ + class CallSystemCommand : public IServerCommand + { + private: + ServerContext& context_; + std::string command_; + std::vector<std::string> arguments_; + + public: + CallSystemCommand(ServerContext& context, + const std::string& command, + const std::vector<std::string>& arguments); + + virtual bool Apply(ListOfStrings& outputs, + const ListOfStrings& inputs); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/DeleteInstanceCommand.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,60 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "DeleteInstanceCommand.h" + +#include <glog/logging.h> + +namespace Orthanc +{ + bool DeleteInstanceCommand::Apply(ListOfStrings& outputs, + const ListOfStrings& inputs) + { + for (ListOfStrings::const_iterator + it = inputs.begin(); it != inputs.end(); ++it) + { + LOG(INFO) << "Deleting instance " << *it; + + try + { + Json::Value tmp; + context_.DeleteResource(tmp, *it, ResourceType_Instance); + } + catch (OrthancException& e) + { + LOG(ERROR) << "Unable to delete instance " << *it << " in a Lua script: " << e.What(); + } + } + + return true; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/DeleteInstanceCommand.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,53 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "IServerCommand.h" +#include "../ServerContext.h" + +namespace Orthanc +{ + class DeleteInstanceCommand : public IServerCommand + { + private: + ServerContext& context_; + + public: + DeleteInstanceCommand(ServerContext& context) : context_(context) + { + } + + virtual bool Apply(ListOfStrings& outputs, + const ListOfStrings& inputs); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/IServerCommand.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,53 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 <string> +#include <boost/noncopyable.hpp> + +namespace Orthanc +{ + class IServerCommand : public boost::noncopyable + { + public: + typedef std::list<std::string> ListOfStrings; + + virtual ~IServerCommand() + { + } + + virtual bool Apply(ListOfStrings& outputs, + const ListOfStrings& inputs) = 0; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/ModifyInstanceCommand.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,103 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "ModifyInstanceCommand.h" + +#include <glog/logging.h> + +namespace Orthanc +{ + ModifyInstanceCommand::ModifyInstanceCommand(ServerContext& context, + const DicomModification& modification) : + context_(context), + modification_(modification) + { + modification_.SetAllowManualIdentifiers(true); + + if (modification_.IsReplaced(DICOM_TAG_PATIENT_ID)) + { + modification_.SetLevel(ResourceType_Patient); + } + else if (modification_.IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID)) + { + modification_.SetLevel(ResourceType_Study); + } + else if (modification_.IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID)) + { + modification_.SetLevel(ResourceType_Series); + } + else + { + modification_.SetLevel(ResourceType_Instance); + } + } + + + bool ModifyInstanceCommand::Apply(ListOfStrings& outputs, + const ListOfStrings& inputs) + { + for (ListOfStrings::const_iterator + it = inputs.begin(); it != inputs.end(); ++it) + { + LOG(INFO) << "Modifying resource " << *it; + + try + { + std::auto_ptr<ParsedDicomFile> modified; + + { + ServerContext::DicomCacheLocker lock(context_, *it); + modified.reset(lock.GetDicom().Clone()); + } + + modification_.Apply(*modified); + + DicomInstanceToStore toStore; + toStore.SetParsedDicomFile(*modified); + // TODO other metadata + toStore.AddMetadata(ResourceType_Instance, MetadataType_ModifiedFrom, *it); + + std::string modifiedId; + context_.Store(modifiedId, toStore); + + // Only chain with other commands if this command succeeds + outputs.push_back(modifiedId); + } + catch (OrthancException& e) + { + LOG(ERROR) << "Unable to modify instance " << *it << " in a Lua script: " << e.What(); + } + } + + return true; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/ModifyInstanceCommand.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,59 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "IServerCommand.h" +#include "../ServerContext.h" +#include "../DicomModification.h" + +namespace Orthanc +{ + class ModifyInstanceCommand : public IServerCommand + { + private: + ServerContext& context_; + DicomModification modification_; + + public: + ModifyInstanceCommand(ServerContext& context, + const DicomModification& modification); + + const DicomModification& GetModification() const + { + return modification_; + } + + virtual bool Apply(ListOfStrings& outputs, + const ListOfStrings& inputs); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/ServerCommandInstance.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,97 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "ServerCommandInstance.h" + +#include "../../Core/OrthancException.h" + +namespace Orthanc +{ + bool ServerCommandInstance::Execute(IListener& listener) + { + ListOfStrings outputs; + + bool success = false; + + try + { + if (command_->Apply(outputs, inputs_)) + { + success = true; + } + } + catch (OrthancException&) + { + } + + if (!success) + { + listener.SignalFailure(jobId_); + return true; + } + + for (std::list<ServerCommandInstance*>::iterator + it = next_.begin(); it != next_.end(); ++it) + { + for (ListOfStrings::const_iterator + output = outputs.begin(); output != outputs.end(); ++output) + { + (*it)->AddInput(*output); + } + } + + listener.SignalSuccess(jobId_); + return true; + } + + + ServerCommandInstance::ServerCommandInstance(IServerCommand *command, + const std::string& jobId) : + command_(command), + jobId_(jobId), + connectedToSink_(false) + { + if (command_ == NULL) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + ServerCommandInstance::~ServerCommandInstance() + { + if (command_ != NULL) + { + delete command_; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/ServerCommandInstance.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,104 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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/IDynamicObject.h" +#include "IServerCommand.h" + +namespace Orthanc +{ + class ServerCommandInstance : public IDynamicObject + { + friend class ServerScheduler; + + public: + class IListener + { + public: + virtual ~IListener() + { + } + + virtual void SignalSuccess(const std::string& jobId) = 0; + + virtual void SignalFailure(const std::string& jobId) = 0; + }; + + private: + typedef IServerCommand::ListOfStrings ListOfStrings; + + IServerCommand *command_; + std::string jobId_; + ListOfStrings inputs_; + std::list<ServerCommandInstance*> next_; + bool connectedToSink_; + + bool Execute(IListener& listener); + + public: + ServerCommandInstance(IServerCommand *command, + const std::string& jobId); + + virtual ~ServerCommandInstance(); + + const std::string& GetJobId() const + { + return jobId_; + } + + void AddInput(const std::string& input) + { + inputs_.push_back(input); + } + + void ConnectOutput(ServerCommandInstance& next) + { + next_.push_back(&next); + } + + void SetConnectedToSink(bool connected = true) + { + connectedToSink_ = connected; + } + + bool IsConnectedToSink() const + { + return connectedToSink_; + } + + const std::list<ServerCommandInstance*>& GetNextCommands() const + { + return next_; + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/ServerJob.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,146 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "ServerJob.h" + +#include "../../Core/OrthancException.h" +#include "../../Core/Toolbox.h" +#include "../../Core/Uuid.h" + +namespace Orthanc +{ + void ServerJob::CheckOrdering() + { + std::map<ServerCommandInstance*, unsigned int> index; + + unsigned int count = 0; + for (std::list<ServerCommandInstance*>::const_iterator + it = filters_.begin(); it != filters_.end(); ++it) + { + index[*it] = count++; + } + + for (std::list<ServerCommandInstance*>::const_iterator + it = filters_.begin(); it != filters_.end(); ++it) + { + const std::list<ServerCommandInstance*>& nextCommands = (*it)->GetNextCommands(); + + for (std::list<ServerCommandInstance*>::const_iterator + next = nextCommands.begin(); next != nextCommands.end(); ++next) + { + if (index.find(*next) == index.end() || + index[*next] <= index[*it]) + { + // You must reorder your calls to "ServerJob::AddCommand" + throw OrthancException("Bad ordering of filters in a job"); + } + } + } + } + + + size_t ServerJob::Submit(SharedMessageQueue& target, + ServerCommandInstance::IListener& listener) + { + if (submitted_) + { + // This job has already been submitted + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + CheckOrdering(); + + size_t size = filters_.size(); + + for (std::list<ServerCommandInstance*>::iterator + it = filters_.begin(); it != filters_.end(); ++it) + { + target.Enqueue(*it); + } + + filters_.clear(); + submitted_ = true; + + return size; + } + + + ServerJob::ServerJob() : + jobId_(Toolbox::GenerateUuid()), + submitted_(false), + description_("no description") + { + } + + + ServerJob::~ServerJob() + { + for (std::list<ServerCommandInstance*>::iterator + it = filters_.begin(); it != filters_.end(); ++it) + { + delete *it; + } + + for (std::list<IDynamicObject*>::iterator + it = payloads_.begin(); it != payloads_.end(); ++it) + { + delete *it; + } + } + + + ServerCommandInstance& ServerJob::AddCommand(IServerCommand* filter) + { + if (submitted_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + filters_.push_back(new ServerCommandInstance(filter, jobId_)); + + return *filters_.back(); + } + + + IDynamicObject& ServerJob::AddPayload(IDynamicObject* payload) + { + if (submitted_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + payloads_.push_back(payload); + + return *filters_.back(); + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/ServerJob.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,82 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "ServerCommandInstance.h" +#include "../../Core/MultiThreading/SharedMessageQueue.h" + +namespace Orthanc +{ + class ServerJob + { + friend class ServerScheduler; + + private: + std::list<ServerCommandInstance*> filters_; + std::list<IDynamicObject*> payloads_; + std::string jobId_; + bool submitted_; + std::string description_; + + void CheckOrdering(); + + size_t Submit(SharedMessageQueue& target, + ServerCommandInstance::IListener& listener); + + public: + ServerJob(); + + ~ServerJob(); + + const std::string& GetId() const + { + return jobId_; + } + + void SetDescription(const std::string& description) + { + description_ = description; + } + + const std::string& GetDescription() const + { + return description_; + } + + ServerCommandInstance& AddCommand(IServerCommand* filter); + + // Take the ownership of a payload to a job. This payload will be + // automatically freed when the job succeeds or fails. + IDynamicObject& AddPayload(IDynamicObject* payload); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/ServerScheduler.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,336 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "ServerScheduler.h" + +#include "../../Core/OrthancException.h" + +#include <glog/logging.h> + +namespace Orthanc +{ + namespace + { + // Anonymous namespace to avoid clashes between compilation modules + class Sink : public IServerCommand + { + private: + ListOfStrings& target_; + + public: + Sink(ListOfStrings& target) : target_(target) + { + } + + virtual bool Apply(ListOfStrings& outputs, + const ListOfStrings& inputs) + { + for (ListOfStrings::const_iterator + it = inputs.begin(); it != inputs.end(); ++it) + { + target_.push_back(*it); + } + + return true; + } + }; + } + + + ServerScheduler::JobInfo& ServerScheduler::GetJobInfo(const std::string& jobId) + { + Jobs::iterator info = jobs_.find(jobId); + + if (info == jobs_.end()) + { + throw OrthancException(ErrorCode_InternalError); + } + + return info->second; + } + + + void ServerScheduler::SignalSuccess(const std::string& jobId) + { + boost::mutex::scoped_lock lock(mutex_); + + JobInfo& info = GetJobInfo(jobId); + info.success_++; + + assert(info.failures_ == 0); + + if (info.success_ >= info.size_) + { + if (info.watched_) + { + watchedJobStatus_[jobId] = JobStatus_Success; + watchedJobFinished_.notify_all(); + } + + LOG(INFO) << "Job successfully finished (" << info.description_ << ")"; + jobs_.erase(jobId); + + availableJob_.Release(); + } + } + + + void ServerScheduler::SignalFailure(const std::string& jobId) + { + boost::mutex::scoped_lock lock(mutex_); + + JobInfo& info = GetJobInfo(jobId); + info.failures_++; + + if (info.success_ + info.failures_ >= info.size_) + { + if (info.watched_) + { + watchedJobStatus_[jobId] = JobStatus_Failure; + watchedJobFinished_.notify_all(); + } + + LOG(ERROR) << "Job has failed (" << info.description_ << ")"; + jobs_.erase(jobId); + + availableJob_.Release(); + } + } + + + void ServerScheduler::Worker(ServerScheduler* that) + { + static const int32_t TIMEOUT = 100; + + LOG(WARNING) << "The server scheduler has started"; + + while (!that->finish_) + { + std::auto_ptr<IDynamicObject> object(that->queue_.Dequeue(TIMEOUT)); + if (object.get() != NULL) + { + ServerCommandInstance& filter = dynamic_cast<ServerCommandInstance&>(*object); + + // Skip the execution of this filter if its parent job has + // previously failed. + bool jobHasFailed; + { + boost::mutex::scoped_lock lock(that->mutex_); + JobInfo& info = that->GetJobInfo(filter.GetJobId()); + jobHasFailed = (info.failures_ > 0 || info.cancel_); + } + + if (jobHasFailed) + { + that->SignalFailure(filter.GetJobId()); + } + else + { + filter.Execute(*that); + } + } + } + } + + + void ServerScheduler::SubmitInternal(ServerJob& job, + bool watched) + { + availableJob_.Acquire(); + + boost::mutex::scoped_lock lock(mutex_); + + JobInfo info; + info.size_ = job.Submit(queue_, *this); + info.cancel_ = false; + info.success_ = 0; + info.failures_ = 0; + info.description_ = job.GetDescription(); + info.watched_ = watched; + + assert(info.size_ > 0); + + if (watched) + { + watchedJobStatus_[job.GetId()] = JobStatus_Running; + } + + jobs_[job.GetId()] = info; + + LOG(INFO) << "New job submitted (" << job.description_ << ")"; + } + + + ServerScheduler::ServerScheduler(unsigned int maxJobs) : availableJob_(maxJobs) + { + finish_ = false; + worker_ = boost::thread(Worker, this); + } + + + ServerScheduler::~ServerScheduler() + { + finish_ = true; + worker_.join(); + } + + + void ServerScheduler::Submit(ServerJob& job) + { + if (job.filters_.empty()) + { + return; + } + + SubmitInternal(job, false); + } + + + bool ServerScheduler::SubmitAndWait(ListOfStrings& outputs, + ServerJob& job) + { + std::string jobId = job.GetId(); + + outputs.clear(); + + if (job.filters_.empty()) + { + return true; + } + + // Add a sink filter to collect all the results of the filters + // that have no next filter. + ServerCommandInstance& sink = job.AddCommand(new Sink(outputs)); + + for (std::list<ServerCommandInstance*>::iterator + it = job.filters_.begin(); it != job.filters_.end(); ++it) + { + if ((*it) != &sink && + (*it)->IsConnectedToSink()) + { + (*it)->ConnectOutput(sink); + } + } + + // Submit the job + SubmitInternal(job, true); + + // Wait for the job to complete (either success or failure) + JobStatus status; + + { + boost::mutex::scoped_lock lock(mutex_); + + assert(watchedJobStatus_.find(jobId) != watchedJobStatus_.end()); + + while (watchedJobStatus_[jobId] == JobStatus_Running) + { + watchedJobFinished_.wait(lock); + } + + status = watchedJobStatus_[jobId]; + watchedJobStatus_.erase(jobId); + } + + return (status == JobStatus_Success); + } + + + bool ServerScheduler::SubmitAndWait(ServerJob& job) + { + ListOfStrings ignoredSink; + return SubmitAndWait(ignoredSink, job); + } + + + bool ServerScheduler::IsRunning(const std::string& jobId) + { + boost::mutex::scoped_lock lock(mutex_); + return jobs_.find(jobId) != jobs_.end(); + } + + + void ServerScheduler::Cancel(const std::string& jobId) + { + boost::mutex::scoped_lock lock(mutex_); + + Jobs::iterator job = jobs_.find(jobId); + + if (job != jobs_.end()) + { + job->second.cancel_ = true; + LOG(WARNING) << "Canceling a job (" << job->second.description_ << ")"; + } + } + + + float ServerScheduler::GetProgress(const std::string& jobId) + { + boost::mutex::scoped_lock lock(mutex_); + + Jobs::iterator job = jobs_.find(jobId); + + if (job == jobs_.end() || + job->second.size_ == 0 /* should never happen */) + { + // This job is not running + return 1; + } + + if (job->second.failures_ != 0) + { + return 1; + } + + if (job->second.size_ == 1) + { + return job->second.success_; + } + + return (static_cast<float>(job->second.success_) / + static_cast<float>(job->second.size_ - 1)); + } + + + void ServerScheduler::GetListOfJobs(ListOfStrings& jobs) + { + boost::mutex::scoped_lock lock(mutex_); + + jobs.clear(); + + for (Jobs::const_iterator + it = jobs_.begin(); it != jobs_.end(); ++it) + { + jobs.push_back(it->first); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/ServerScheduler.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,120 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "ServerJob.h" + +#include "../../Core/MultiThreading/Semaphore.h" + +namespace Orthanc +{ + class ServerScheduler : public ServerCommandInstance::IListener + { + private: + struct JobInfo + { + bool watched_; + bool cancel_; + size_t size_; + size_t success_; + size_t failures_; + std::string description_; + }; + + enum JobStatus + { + JobStatus_Running = 1, + JobStatus_Success = 2, + JobStatus_Failure = 3 + }; + + typedef IServerCommand::ListOfStrings ListOfStrings; + typedef std::map<std::string, JobInfo> Jobs; + + boost::mutex mutex_; + boost::condition_variable watchedJobFinished_; + Jobs jobs_; + SharedMessageQueue queue_; + bool finish_; + boost::thread worker_; + std::map<std::string, JobStatus> watchedJobStatus_; + Semaphore availableJob_; + + JobInfo& GetJobInfo(const std::string& jobId); + + virtual void SignalSuccess(const std::string& jobId); + + virtual void SignalFailure(const std::string& jobId); + + static void Worker(ServerScheduler* that); + + void SubmitInternal(ServerJob& job, + bool watched); + + public: + ServerScheduler(unsigned int maxjobs); + + ~ServerScheduler(); + + void Submit(ServerJob& job); + + bool SubmitAndWait(ListOfStrings& outputs, + ServerJob& job); + + bool SubmitAndWait(ServerJob& job); + + bool IsRunning(const std::string& jobId); + + void Cancel(const std::string& jobId); + + // Returns a number between 0 and 1 + float GetProgress(const std::string& jobId); + + bool IsRunning(const ServerJob& job) + { + return IsRunning(job.GetId()); + } + + void Cancel(const ServerJob& job) + { + Cancel(job.GetId()); + } + + float GetProgress(const ServerJob& job) + { + return GetProgress(job.GetId()); + } + + void GetListOfJobs(ListOfStrings& jobs); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/StorePeerCommand.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,100 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "StorePeerCommand.h" + +#include "../../Core/HttpClient.h" + +#include <glog/logging.h> + +namespace Orthanc +{ + StorePeerCommand::StorePeerCommand(ServerContext& context, + const OrthancPeerParameters& peer, + bool ignoreExceptions) : + context_(context), + peer_(peer), + ignoreExceptions_(ignoreExceptions) + { + } + + bool StorePeerCommand::Apply(ListOfStrings& outputs, + const ListOfStrings& inputs) + { + // Configure the HTTP client + HttpClient client; + client.SetProxy(Configuration::GetGlobalStringParameter("HttpProxy", "")); + if (peer_.GetUsername().size() != 0 && + peer_.GetPassword().size() != 0) + { + client.SetCredentials(peer_.GetUsername().c_str(), + peer_.GetPassword().c_str()); + } + + client.SetUrl(peer_.GetUrl() + "instances"); + client.SetMethod(HttpMethod_Post); + + for (ListOfStrings::const_iterator + it = inputs.begin(); it != inputs.end(); ++it) + { + LOG(INFO) << "Sending resource " << *it << " to peer \"" + << peer_.GetUrl() << "\""; + + try + { + context_.ReadFile(client.AccessPostData(), *it, FileContentType_Dicom); + + std::string answer; + if (!client.Apply(answer)) + { + LOG(ERROR) << "Unable to send resource " << *it << " to peer \"" << peer_.GetUrl() << "\""; + throw OrthancException(ErrorCode_NetworkProtocol); + } + + // Only chain with other commands if this command succeeds + outputs.push_back(*it); + } + catch (OrthancException& e) + { + LOG(ERROR) << "Unable to forward to an Orthanc peer in a Lua script (instance " + << *it << ", peer " << peer_.GetUrl() << "): " << e.What(); + + if (!ignoreExceptions_) + { + throw; + } + } + } + + return true; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/StorePeerCommand.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,56 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "IServerCommand.h" +#include "../ServerContext.h" +#include "../OrthancInitialization.h" + +namespace Orthanc +{ + class StorePeerCommand : public IServerCommand + { + private: + ServerContext& context_; + OrthancPeerParameters peer_; + bool ignoreExceptions_; + + public: + StorePeerCommand(ServerContext& context, + const OrthancPeerParameters& peer, + bool ignoreExceptions); + + virtual bool Apply(ListOfStrings& outputs, + const ListOfStrings& inputs); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/StoreScuCommand.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,84 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "StoreScuCommand.h" + +#include <glog/logging.h> + +namespace Orthanc +{ + StoreScuCommand::StoreScuCommand(ServerContext& context, + const RemoteModalityParameters& modality, + bool ignoreExceptions) : + context_(context), + modality_(modality), + ignoreExceptions_(ignoreExceptions) + { + } + + bool StoreScuCommand::Apply(ListOfStrings& outputs, + const ListOfStrings& inputs) + { + ReusableDicomUserConnection::Locker locker(context_.GetReusableDicomUserConnection(), modality_); + + for (ListOfStrings::const_iterator + it = inputs.begin(); it != inputs.end(); ++it) + { + LOG(INFO) << "Sending resource " << *it << " to modality \"" + << modality_.GetApplicationEntityTitle() << "\""; + + try + { + std::string dicom; + context_.ReadFile(dicom, *it, FileContentType_Dicom); + locker.GetConnection().Store(dicom); + + // Only chain with other commands if this command succeeds + outputs.push_back(*it); + } + catch (OrthancException& e) + { + // Ignore transmission errors (e.g. if the remote modality is + // powered off) + LOG(ERROR) << "Unable to forward to a modality in a Lua script (instance " + << *it << "): " << e.What(); + + if (!ignoreExceptions_) + { + throw; + } + } + } + + return true; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Scheduler/StoreScuCommand.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,55 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "IServerCommand.h" +#include "../ServerContext.h" + +namespace Orthanc +{ + class StoreScuCommand : public IServerCommand + { + private: + ServerContext& context_; + RemoteModalityParameters modality_; + bool ignoreExceptions_; + + public: + StoreScuCommand(ServerContext& context, + const RemoteModalityParameters& modality, + bool ignoreExceptions); + + virtual bool Apply(ListOfStrings& outputs, + const ListOfStrings& inputs); + }; +}
--- a/OrthancServer/ServerContext.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/ServerContext.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -43,9 +43,20 @@ #include <EmbeddedResources.h> #include <dcmtk/dcmdata/dcfilefo.h> + +#include "Scheduler/CallSystemCommand.h" +#include "Scheduler/DeleteInstanceCommand.h" +#include "Scheduler/ModifyInstanceCommand.h" +#include "Scheduler/StoreScuCommand.h" +#include "Scheduler/StorePeerCommand.h" +#include "OrthancRestApi/OrthancRestApi.h" +#include "../Plugins/Engine/OrthancPlugins.h" + + #define ENABLE_DICOM_CACHE 1 static const char* RECEIVED_INSTANCE_FILTER = "ReceivedInstanceFilter"; +static const char* ON_STORED_INSTANCE = "OnStoredInstance"; static const size_t DICOM_CACHE_SIZE = 2; @@ -60,19 +71,22 @@ namespace Orthanc { - ServerContext::ServerContext(const boost::filesystem::path& storagePath, - const boost::filesystem::path& indexPath) : - storage_(storagePath.string()), - index_(*this, indexPath.string()), - accessor_(storage_), + ServerContext::ServerContext(IDatabaseWrapper& database) : + index_(*this, database), compressionEnabled_(false), provider_(*this), - dicomCache_(provider_, DICOM_CACHE_SIZE) + dicomCache_(provider_, DICOM_CACHE_SIZE), + scheduler_(Configuration::GetGlobalIntegerParameter("LimitJobs", 10)), + plugins_(NULL), + pluginsManager_(NULL) { scu_.SetLocalApplicationEntityTitle(Configuration::GetGlobalStringParameter("DicomAet", "ORTHANC")); - //scu_.SetMillisecondsBeforeClose(1); // The connection is always released + + uint64_t s = Configuration::GetGlobalIntegerParameter("DicomAssociationCloseDelay", 5); // In seconds + scu_.SetMillisecondsBeforeClose(s * 1000); // Milliseconds are expected here lua_.Execute(Orthanc::EmbeddedResources::LUA_TOOLBOX); + lua_.SetHttpProxy(Configuration::GetGlobalStringParameter("HttpProxy", "")); } void ServerContext::SetCompressionEnabled(bool enabled) @@ -85,84 +99,312 @@ compressionEnabled_ = enabled; } - void ServerContext::RemoveFile(const std::string& fileUuid) + void ServerContext::RemoveFile(const std::string& fileUuid, + FileContentType type) { - storage_.Remove(fileUuid); + accessor_.Remove(fileUuid, type); } - StoreStatus ServerContext::Store(const char* dicomInstance, - size_t dicomSize, - const DicomMap& dicomSummary, - const Json::Value& dicomJson, - const std::string& remoteAet) + + bool ServerContext::ApplyReceivedInstanceFilter(const Json::Value& simplified, + const std::string& remoteAet) { - // Test if the instance must be filtered out - if (lua_.IsExistingFunction(RECEIVED_INSTANCE_FILTER)) + LuaContextLocker locker(*this); + + if (locker.GetLua().IsExistingFunction(RECEIVED_INSTANCE_FILTER)) { - Json::Value simplified; - SimplifyTags(simplified, dicomJson); - - LuaFunctionCall call(lua_, RECEIVED_INSTANCE_FILTER); - call.PushJSON(simplified); + LuaFunctionCall call(locker.GetLua(), RECEIVED_INSTANCE_FILTER); + call.PushJson(simplified); call.PushString(remoteAet); if (!call.ExecutePredicate()) { + return false; + } + } + + return true; + } + + + static IServerCommand* ParseOperation(ServerContext& context, + const std::string& operation, + const Json::Value& parameters) + { + if (operation == "delete") + { + LOG(INFO) << "Lua script to delete instance " << parameters["Instance"].asString(); + return new DeleteInstanceCommand(context); + } + + if (operation == "store-scu") + { + std::string modality = parameters["Modality"].asString(); + LOG(INFO) << "Lua script to send instance " << parameters["Instance"].asString() + << " to modality " << modality << " using Store-SCU"; + return new StoreScuCommand(context, Configuration::GetModalityUsingSymbolicName(modality), true); + } + + if (operation == "store-peer") + { + std::string peer = parameters["Peer"].asString(); + LOG(INFO) << "Lua script to send instance " << parameters["Instance"].asString() + << " to peer " << peer << " using HTTP"; + + OrthancPeerParameters parameters; + Configuration::GetOrthancPeer(parameters, peer); + return new StorePeerCommand(context, parameters, true); + } + + if (operation == "modify") + { + LOG(INFO) << "Lua script to modify instance " << parameters["Instance"].asString(); + DicomModification modification; + OrthancRestApi::ParseModifyRequest(modification, parameters); + + std::auto_ptr<ModifyInstanceCommand> command(new ModifyInstanceCommand(context, modification)); + return command.release(); + } + + if (operation == "call-system") + { + LOG(INFO) << "Lua script to call system command on " << parameters["Instance"].asString(); + + const Json::Value& argsIn = parameters["Arguments"]; + if (argsIn.type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadParameterType); + } + + std::vector<std::string> args; + args.reserve(argsIn.size()); + for (Json::Value::ArrayIndex i = 0; i < argsIn.size(); ++i) + { + // http://jsoncpp.sourceforge.net/namespace_json.html#7d654b75c16a57007925868e38212b4e + switch (argsIn[i].type()) + { + case Json::stringValue: + args.push_back(argsIn[i].asString()); + break; + + case Json::intValue: + args.push_back(boost::lexical_cast<std::string>(argsIn[i].asInt())); + break; + + case Json::uintValue: + args.push_back(boost::lexical_cast<std::string>(argsIn[i].asUInt())); + break; + + case Json::realValue: + args.push_back(boost::lexical_cast<std::string>(argsIn[i].asFloat())); + break; + + default: + throw OrthancException(ErrorCode_BadParameterType); + } + } + + return new CallSystemCommand(context, parameters["Command"].asString(), args); + } + + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + + void ServerContext::ApplyLuaOnStoredInstance(const std::string& instanceId, + const Json::Value& simplifiedDicom, + const Json::Value& metadata, + const std::string& remoteAet, + const std::string& calledAet) + { + LuaContextLocker locker(*this); + + if (locker.GetLua().IsExistingFunction(ON_STORED_INSTANCE)) + { + locker.GetLua().Execute("_InitializeJob()"); + + LuaFunctionCall call(locker.GetLua(), ON_STORED_INSTANCE); + call.PushString(instanceId); + call.PushJson(simplifiedDicom); + call.PushJson(metadata); + call.PushJson(remoteAet); + call.PushJson(calledAet); + call.Execute(); + + Json::Value operations; + LuaFunctionCall call2(locker.GetLua(), "_AccessJob"); + call2.ExecuteToJson(operations); + + if (operations.type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_InternalError); + } + + ServerJob job; + ServerCommandInstance* previousCommand = NULL; + + for (Json::Value::ArrayIndex i = 0; i < operations.size(); ++i) + { + if (operations[i].type() != Json::objectValue || + !operations[i].isMember("Operation")) + { + throw OrthancException(ErrorCode_InternalError); + } + + const Json::Value& parameters = operations[i]; + std::string operation = parameters["Operation"].asString(); + + ServerCommandInstance& command = job.AddCommand(ParseOperation(*this, operation, operations[i])); + + if (!parameters.isMember("Instance")) + { + throw OrthancException(ErrorCode_InternalError); + } + + std::string instance = parameters["Instance"].asString(); + if (instance.empty()) + { + previousCommand->ConnectOutput(command); + } + else + { + command.AddInput(instance); + } + + previousCommand = &command; + } + + job.SetDescription(std::string("Lua script: ") + ON_STORED_INSTANCE); + scheduler_.Submit(job); + } + } + + + StoreStatus ServerContext::Store(std::string& resultPublicId, + DicomInstanceToStore& dicom) + { + try + { + DicomInstanceHasher hasher(dicom.GetSummary()); + resultPublicId = hasher.HashInstance(); + + Json::Value simplified; + SimplifyTags(simplified, dicom.GetJson()); + + // Test if the instance must be filtered out + if (!ApplyReceivedInstanceFilter(simplified, dicom.GetRemoteAet())) + { LOG(INFO) << "An incoming instance has been discarded by the filter"; return StoreStatus_FilteredOut; } - } + + if (compressionEnabled_) + { + accessor_.SetCompressionForNextOperations(CompressionType_Zlib); + } + else + { + accessor_.SetCompressionForNextOperations(CompressionType_None); + } + + FileInfo dicomInfo = accessor_.Write(dicom.GetBufferData(), dicom.GetBufferSize(), FileContentType_Dicom); + FileInfo jsonInfo = accessor_.Write(dicom.GetJson().toStyledString(), FileContentType_DicomAsJson); + + ServerIndex::Attachments attachments; + attachments.push_back(dicomInfo); + attachments.push_back(jsonInfo); + + typedef std::map<MetadataType, std::string> InstanceMetadata; + InstanceMetadata instanceMetadata; + StoreStatus status = index_.Store(instanceMetadata, dicom.GetSummary(), attachments, + dicom.GetRemoteAet(), dicom.GetMetadata()); - if (compressionEnabled_) - { - accessor_.SetCompressionForNextOperations(CompressionType_Zlib); - } - else - { - accessor_.SetCompressionForNextOperations(CompressionType_None); - } + dicom.GetMetadata().clear(); + + for (InstanceMetadata::const_iterator it = instanceMetadata.begin(); + it != instanceMetadata.end(); ++it) + { + dicom.GetMetadata().insert(std::make_pair(std::make_pair(ResourceType_Instance, it->first), + it->second)); + } + + if (status != StoreStatus_Success) + { + accessor_.Remove(dicomInfo.GetUuid(), FileContentType_Dicom); + accessor_.Remove(jsonInfo.GetUuid(), FileContentType_DicomAsJson); + } + + switch (status) + { + case StoreStatus_Success: + LOG(INFO) << "New instance stored"; + break; + + case StoreStatus_AlreadyStored: + LOG(INFO) << "Already stored"; + break; - FileInfo dicomInfo = accessor_.Write(dicomInstance, dicomSize, FileContentType_Dicom); - FileInfo jsonInfo = accessor_.Write(dicomJson.toStyledString(), FileContentType_DicomAsJson); + case StoreStatus_Failure: + LOG(ERROR) << "Store failure"; + break; + + default: + // This should never happen + break; + } + + if (status == StoreStatus_Success || + status == StoreStatus_AlreadyStored) + { + Json::Value metadata = Json::objectValue; + for (std::map<MetadataType, std::string>::const_iterator + it = instanceMetadata.begin(); + it != instanceMetadata.end(); ++it) + { + metadata[EnumerationToString(it->first)] = it->second; + } - ServerIndex::Attachments attachments; - attachments.push_back(dicomInfo); - attachments.push_back(jsonInfo); + try + { + ApplyLuaOnStoredInstance(resultPublicId, simplified, metadata, + dicom.GetRemoteAet(), dicom.GetCalledAet()); + } + catch (OrthancException& e) + { + LOG(ERROR) << "Error in " << ON_STORED_INSTANCE << " callback (Lua): " << e.What(); + } - StoreStatus status = index_.Store(dicomSummary, attachments, remoteAet); + if (plugins_ != NULL) + { + try + { + plugins_->SignalStoredInstance(dicom, resultPublicId); + } + catch (OrthancException& e) + { + LOG(ERROR) << "Error in " << ON_STORED_INSTANCE << " callback (plugins): " << e.What(); + } + } + } - if (status != StoreStatus_Success) + return status; + } + catch (OrthancException& e) { - storage_.Remove(dicomInfo.GetUuid()); - storage_.Remove(jsonInfo.GetUuid()); - } - - switch (status) - { - case StoreStatus_Success: - LOG(INFO) << "New instance stored"; - break; + if (e.GetErrorCode() == ErrorCode_InexistentTag) + { + LogMissingRequiredTag(dicom.GetSummary()); + } - case StoreStatus_AlreadyStored: - LOG(INFO) << "Already stored"; - break; - - case StoreStatus_Failure: - LOG(ERROR) << "Store failure"; - break; - - default: - // This should never happen - break; + throw; } - - return status; } - - void ServerContext::AnswerDicomFile(RestApiOutput& output, - const std::string& instancePublicId, - FileContentType content) + + + void ServerContext::AnswerAttachment(RestApiOutput& output, + const std::string& instancePublicId, + FileContentType content) { FileInfo attachment; if (!index_.LookupAttachment(attachment, instancePublicId, content)) @@ -172,8 +414,8 @@ accessor_.SetCompressionForNextOperations(attachment.GetCompressionType()); - std::auto_ptr<HttpFileSender> sender(accessor_.ConstructHttpFileSender(attachment.GetUuid())); - sender->SetContentType("application/dicom"); + std::auto_ptr<HttpFileSender> sender(accessor_.ConstructHttpFileSender(attachment.GetUuid(), attachment.GetContentType())); + sender->SetContentType(GetMimeType(content)); sender->SetDownloadFilename(instancePublicId + ".dcm"); output.AnswerFile(*sender); } @@ -213,7 +455,7 @@ accessor_.SetCompressionForNextOperations(CompressionType_None); } - accessor_.Read(result, attachment.GetUuid()); + accessor_.Read(result, attachment.GetUuid(), attachment.GetContentType()); } @@ -227,14 +469,14 @@ ServerContext::DicomCacheLocker::DicomCacheLocker(ServerContext& that, const std::string& instancePublicId) : - that_(that) + that_(that), + lock_(that_.dicomCacheMutex_) { #if ENABLE_DICOM_CACHE == 0 static std::auto_ptr<IDynamicObject> p; p.reset(provider_.Provide(instancePublicId)); dicom_ = dynamic_cast<ParsedDicomFile*>(p.get()); #else - that_.dicomCacheMutex_.lock(); dicom_ = &dynamic_cast<ParsedDicomFile&>(that_.dicomCache_.Access(instancePublicId)); #endif } @@ -242,91 +484,6 @@ ServerContext::DicomCacheLocker::~DicomCacheLocker() { -#if ENABLE_DICOM_CACHE == 0 -#else - that_.dicomCacheMutex_.unlock(); -#endif - } - - - static DcmFileFormat& GetDicom(ParsedDicomFile& file) - { - return *reinterpret_cast<DcmFileFormat*>(file.GetDcmtkObject()); - } - - - StoreStatus ServerContext::Store(std::string& resultPublicId, - ParsedDicomFile& dicomInstance, - const char* dicomBuffer, - size_t dicomSize) - { - DicomMap dicomSummary; - FromDcmtkBridge::Convert(dicomSummary, *GetDicom(dicomInstance).getDataset()); - - try - { - DicomInstanceHasher hasher(dicomSummary); - resultPublicId = hasher.HashInstance(); - - Json::Value dicomJson; - FromDcmtkBridge::ToJson(dicomJson, *GetDicom(dicomInstance).getDataset()); - - StoreStatus status = StoreStatus_Failure; - if (dicomSize > 0) - { - status = Store(dicomBuffer, dicomSize, dicomSummary, dicomJson, ""); - } - - return status; - } - catch (OrthancException& e) - { - if (e.GetErrorCode() == ErrorCode_InexistentTag) - { - LogMissingRequiredTag(dicomSummary); - } - - throw; - } - } - - - StoreStatus ServerContext::Store(std::string& resultPublicId, - ParsedDicomFile& dicomInstance) - { - std::string buffer; - if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer, GetDicom(dicomInstance).getDataset())) - { - throw OrthancException(ErrorCode_InternalError); - } - - if (buffer.size() == 0) - return Store(resultPublicId, dicomInstance, NULL, 0); - else - return Store(resultPublicId, dicomInstance, &buffer[0], buffer.size()); - } - - - StoreStatus ServerContext::Store(std::string& resultPublicId, - const char* dicomBuffer, - size_t dicomSize) - { - ParsedDicomFile dicom(dicomBuffer, dicomSize); - return Store(resultPublicId, dicom, dicomBuffer, dicomSize); - } - - - StoreStatus ServerContext::Store(std::string& resultPublicId, - const std::string& dicomContent) - { - if (dicomContent.size() == 0) - { - return Store(resultPublicId, NULL, 0); - } - else - { - return Store(resultPublicId, &dicomContent[0], dicomContent.size()); - } } @@ -358,7 +515,7 @@ if (status != StoreStatus_Success) { - storage_.Remove(info.GetUuid()); + accessor_.Remove(info.GetUuid(), info.GetContentType()); return false; } else @@ -366,4 +523,60 @@ return true; } } + + + bool ServerContext::DeleteResource(Json::Value& target, + const std::string& uuid, + ResourceType expectedType) + { + return index_.DeleteResource(target, uuid, expectedType); + } + + + void ServerContext::SignalChange(const ServerIndexChange& change) + { + if (plugins_ != NULL) + { + try + { + plugins_->SignalChange(change); + } + catch (OrthancException& e) + { + LOG(ERROR) << "Error in OnChangeCallback (plugins): " << e.What(); + } + } + } + + + bool ServerContext::HasPlugins() const + { + return (pluginsManager_ && plugins_); + } + + + const PluginsManager& ServerContext::GetPluginsManager() const + { + if (HasPlugins()) + { + return *pluginsManager_; + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + + + const OrthancPlugins& ServerContext::GetOrthancPlugins() const + { + if (HasPlugins()) + { + return *plugins_; + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } }
--- a/OrthancServer/ServerContext.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/ServerContext.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -34,15 +34,23 @@ #include "../Core/Cache/MemoryCache.h" #include "../Core/FileStorage/CompressedFileStorageAccessor.h" -#include "../Core/FileStorage/FileStorage.h" +#include "../Core/FileStorage/IStorageArea.h" #include "../Core/RestApi/RestApiOutput.h" #include "../Core/Lua/LuaContext.h" #include "ServerIndex.h" #include "ParsedDicomFile.h" #include "DicomProtocol/ReusableDicomUserConnection.h" +#include "Scheduler/ServerScheduler.h" +#include "DicomInstanceToStore.h" +#include "ServerIndexChange.h" + +#include <boost/filesystem.hpp> namespace Orthanc { + class OrthancPlugins; + class PluginsManager; + /** * This class is responsible for maintaining the storage area on the * filesystem (including compression), as well as the index of the @@ -64,7 +72,15 @@ virtual IDynamicObject* Provide(const std::string& id); }; - FileStorage storage_; + bool ApplyReceivedInstanceFilter(const Json::Value& simplified, + const std::string& remoteAet); + + void ApplyLuaOnStoredInstance(const std::string& instanceId, + const Json::Value& simplifiedDicom, + const Json::Value& metadata, + const std::string& remoteAet, + const std::string& calledAet); + ServerIndex index_; CompressedFileStorageAccessor accessor_; bool compressionEnabled_; @@ -73,15 +89,20 @@ boost::mutex dicomCacheMutex_; MemoryCache dicomCache_; ReusableDicomUserConnection scu_; + ServerScheduler scheduler_; + boost::mutex luaMutex_; LuaContext lua_; + OrthancPlugins* plugins_; // TODO Turn it into a listener pattern (idem for Lua callbacks) + const PluginsManager* pluginsManager_; public: - class DicomCacheLocker + class DicomCacheLocker : public boost::noncopyable { private: ServerContext& that_; ParsedDicomFile *dicom_; + boost::mutex::scoped_lock lock_; public: DicomCacheLocker(ServerContext& that, @@ -95,8 +116,35 @@ } }; - ServerContext(const boost::filesystem::path& storagePath, - const boost::filesystem::path& indexPath); + class LuaContextLocker : public boost::noncopyable + { + private: + ServerContext& that_; + + public: + LuaContextLocker(ServerContext& that) : that_(that) + { + that.luaMutex_.lock(); + } + + ~LuaContextLocker() + { + that_.luaMutex_.unlock(); + } + + LuaContext& GetLua() + { + return that_.lua_; + } + }; + + + ServerContext(IDatabaseWrapper& database); + + void SetStorageArea(IStorageArea& storage) + { + accessor_.SetStorageArea(storage); + } ServerIndex& GetIndex() { @@ -110,37 +158,20 @@ return compressionEnabled_; } - void RemoveFile(const std::string& fileUuid); + void RemoveFile(const std::string& fileUuid, + FileContentType type); bool AddAttachment(const std::string& resourceId, FileContentType attachmentType, const void* data, size_t size); - StoreStatus Store(const char* dicomInstance, - size_t dicomSize, - const DicomMap& dicomSummary, - const Json::Value& dicomJson, - const std::string& remoteAet); - StoreStatus Store(std::string& resultPublicId, - ParsedDicomFile& dicomInstance, - const char* dicomBuffer, - size_t dicomSize); + DicomInstanceToStore& dicom); - StoreStatus Store(std::string& resultPublicId, - ParsedDicomFile& dicomInstance); - - StoreStatus Store(std::string& resultPublicId, - const char* dicomBuffer, - size_t dicomSize); - - StoreStatus Store(std::string& resultPublicId, - const std::string& dicomContent); - - void AnswerDicomFile(RestApiOutput& output, - const std::string& instancePublicId, - FileContentType content); + void AnswerAttachment(RestApiOutput& output, + const std::string& instancePublicId, + FileContentType content); void ReadJson(Json::Value& result, const std::string& instancePublicId); @@ -151,11 +182,6 @@ FileContentType content, bool uncompressIfNeeded = true); - LuaContext& GetLuaContext() - { - return lua_; - } - void SetStoreMD5ForAttachments(bool storeMD5); bool IsStoreMD5ForAttachments() const @@ -167,5 +193,35 @@ { return scu_; } + + ServerScheduler& GetScheduler() + { + return scheduler_; + } + + void SetOrthancPlugins(const PluginsManager& manager, + OrthancPlugins& plugins) + { + pluginsManager_ = &manager; + plugins_ = &plugins; + } + + void ResetOrthancPlugins() + { + pluginsManager_ = NULL; + plugins_ = NULL; + } + + bool DeleteResource(Json::Value& target, + const std::string& uuid, + ResourceType expectedType); + + void SignalChange(const ServerIndexChange& change); + + bool HasPlugins() const; + + const PluginsManager& GetPluginsManager() const; + + const OrthancPlugins& GetOrthancPlugins() const; }; }
--- a/OrthancServer/ServerEnumerations.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/ServerEnumerations.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -48,6 +48,9 @@ void InitializeServerEnumerations() { boost::mutex::scoped_lock lock(enumerationsMutex_); + + dictMetadataType_.Clear(); + dictContentType_.Clear(); dictMetadataType_.Add(MetadataType_Instance_IndexInSeries, "IndexInSeries"); dictMetadataType_.Add(MetadataType_Instance_ReceptionDate, "ReceptionDate"); @@ -228,6 +231,12 @@ case ChangeType_StableSeries: return "StableSeries"; + case ChangeType_Deleted: + return "Deleted"; + + case ChangeType_NewChildInstance: + return "NewChildInstance"; + default: throw OrthancException(ErrorCode_ParameterOutOfRange); } @@ -279,6 +288,9 @@ case ModalityManufacturer_Generic: return "Generic"; + case ModalityManufacturer_StoreScp: + return "StoreScp"; + case ModalityManufacturer_ClearCanvas: return "ClearCanvas"; @@ -318,7 +330,6 @@ return "Store"; break; - default: throw OrthancException(ErrorCode_ParameterOutOfRange); } @@ -336,6 +347,10 @@ { return ModalityManufacturer_ClearCanvas; } + else if (manufacturer == "StoreScp") + { + return ModalityManufacturer_StoreScp; + } else if (manufacturer == "MedInria") { return ModalityManufacturer_MedInria; @@ -349,4 +364,36 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } } + + + const char* EnumerationToString(TransferSyntax syntax) + { + switch (syntax) + { + case TransferSyntax_Deflated: + return "Deflated"; + + case TransferSyntax_Jpeg: + return "JPEG"; + + case TransferSyntax_Jpeg2000: + return "JPEG2000"; + + case TransferSyntax_JpegLossless: + return "JPEG Lossless"; + + case TransferSyntax_Jpip: + return "JPIP"; + + case TransferSyntax_Mpeg2: + return "MPEG2"; + + case TransferSyntax_Rle: + return "RLE"; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + }
--- a/OrthancServer/ServerEnumerations.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/ServerEnumerations.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -32,6 +32,7 @@ #pragma once #include <string> +#include <map> #include "../Core/Enumerations.h" @@ -56,6 +57,7 @@ enum ModalityManufacturer { ModalityManufacturer_Generic, + ModalityManufacturer_StoreScp, ModalityManufacturer_ClearCanvas, ModalityManufacturer_MedInria, ModalityManufacturer_Dcm4Chee @@ -77,6 +79,17 @@ DicomReplaceMode_IgnoreIfAbsent }; + enum TransferSyntax + { + TransferSyntax_Deflated, + TransferSyntax_Jpeg, + TransferSyntax_Jpeg2000, + TransferSyntax_JpegLossless, + TransferSyntax_Jpip, + TransferSyntax_Mpeg2, + TransferSyntax_Rle + }; + /** * WARNING: Do not change the explicit values in the enumerations @@ -121,9 +134,17 @@ ChangeType_ModifiedPatient = 11, ChangeType_StablePatient = 12, ChangeType_StableStudy = 13, - ChangeType_StableSeries = 14 + ChangeType_StableSeries = 14, + + ChangeType_INTERNAL_LastLogged = 4095, + + // The changes below this point are not logged into the database + ChangeType_Deleted = 4096, + ChangeType_NewChildInstance = 4097 }; + + void InitializeServerEnumerations(); void RegisterUserMetadata(int metadata, @@ -153,6 +174,8 @@ const char* EnumerationToString(DicomRequestType type); + const char* EnumerationToString(TransferSyntax syntax); + ModalityManufacturer StringToModalityManufacturer(const std::string& manufacturer); ResourceType GetParentResourceType(ResourceType type);
--- a/OrthancServer/ServerIndex.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/ServerIndex.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -37,12 +37,12 @@ #define NOMINMAX #endif +#include "ServerIndexChange.h" #include "EmbeddedResources.h" #include "OrthancInitialization.h" #include "../Core/Toolbox.h" #include "../Core/Uuid.h" #include "../Core/DicomFormat/DicomArray.h" -#include "../Core/SQLite/Transaction.h" #include "FromDcmtkBridge.h" #include "ServerContext.h" @@ -59,16 +59,49 @@ class ServerIndexListener : public IServerIndexListener { private: + struct FileToRemove + { + private: + std::string uuid_; + FileContentType type_; + + public: + FileToRemove(const FileInfo& info) : uuid_(info.GetUuid()), + type_(info.GetContentType()) + { + } + + const std::string& GetUuid() const + { + return uuid_; + } + + FileContentType GetContentType() const + { + return type_; + } + }; + ServerContext& context_; bool hasRemainingLevel_; ResourceType remainingType_; std::string remainingPublicId_; - std::list<std::string> pendingFilesToRemove_; + std::list<FileToRemove> pendingFilesToRemove_; + std::list<ServerIndexChange> pendingChanges_; uint64_t sizeOfFilesToRemove_; + bool insideTransaction_; + + void Reset() + { + sizeOfFilesToRemove_ = 0; + hasRemainingLevel_ = false; + pendingFilesToRemove_.clear(); + pendingChanges_.clear(); + } public: - ServerIndexListener(ServerContext& context) : - context_(context) + ServerIndexListener(ServerContext& context) : context_(context), + insideTransaction_(false) { Reset(); assert(ResourceType_Patient < ResourceType_Study && @@ -76,11 +109,15 @@ ResourceType_Series < ResourceType_Instance); } - void Reset() + void StartTransaction() { - sizeOfFilesToRemove_ = 0; - hasRemainingLevel_ = false; - pendingFilesToRemove_.clear(); + Reset(); + insideTransaction_ = true; + } + + void EndTransaction() + { + insideTransaction_ = false; } uint64_t GetSizeOfFilesToRemove() @@ -90,18 +127,28 @@ void CommitFilesToRemove() { - for (std::list<std::string>::iterator + for (std::list<FileToRemove>::const_iterator it = pendingFilesToRemove_.begin(); it != pendingFilesToRemove_.end(); ++it) { - context_.RemoveFile(*it); + context_.RemoveFile(it->GetUuid(), it->GetContentType()); + } + } + + void CommitChanges() + { + for (std::list<ServerIndexChange>::const_iterator + it = pendingChanges_.begin(); + it != pendingChanges_.end(); ++it) + { + context_.SignalChange(*it); } } virtual void SignalRemainingAncestor(ResourceType parentType, const std::string& publicId) { - LOG(INFO) << "Remaining ancestor \"" << publicId << "\" (" << parentType << ")"; + VLOG(1) << "Remaining ancestor \"" << publicId << "\" (" << parentType << ")"; if (hasRemainingLevel_) { @@ -122,10 +169,26 @@ virtual void SignalFileDeleted(const FileInfo& info) { assert(Toolbox::IsUuid(info.GetUuid())); - pendingFilesToRemove_.push_back(info.GetUuid()); + pendingFilesToRemove_.push_back(FileToRemove(info)); sizeOfFilesToRemove_ += info.GetCompressedSize(); } + virtual void SignalChange(const ServerIndexChange& change) + { + VLOG(1) << "Change related to resource " << change.GetPublicId() << " of type " + << EnumerationToString(change.GetResourceType()) << ": " + << EnumerationToString(change.GetChangeType()); + + if (insideTransaction_) + { + pendingChanges_.push_back(change); + } + else + { + context_.SignalChange(change); + } + } + bool HasRemainingLevel() const { return hasRemainingLevel_; @@ -150,7 +213,7 @@ { private: ServerIndex& index_; - std::auto_ptr<SQLite::Transaction> transaction_; + std::auto_ptr<SQLite::ITransaction> transaction_; bool isCommitted_; public: @@ -158,11 +221,22 @@ index_(index), isCommitted_(false) { - assert(index_.currentStorageSize_ == index_.db_->GetTotalCompressedSize()); + transaction_.reset(index_.db_.StartTransaction()); + transaction_->Begin(); + + assert(index_.currentStorageSize_ == index_.db_.GetTotalCompressedSize()); + + index_.listener_->StartTransaction(); + } - index_.listener_->Reset(); - transaction_.reset(index_.db_->StartTransaction()); - transaction_->Begin(); + ~Transaction() + { + index_.listener_->EndTransaction(); + + if (!isCommitted_) + { + transaction_->Rollback(); + } } void Commit(uint64_t sizeOfAddedFiles) @@ -181,7 +255,8 @@ assert(index_.currentStorageSize_ >= index_.listener_->GetSizeOfFilesToRemove()); index_.currentStorageSize_ -= index_.listener_->GetSizeOfFilesToRemove(); - assert(index_.currentStorageSize_ == index_.db_->GetTotalCompressedSize()); + // Send all the pending changes to the Orthanc plugins + index_.listener_->CommitChanges(); isCommitted_ = true; } @@ -189,16 +264,22 @@ }; - struct ServerIndex::UnstableResourcePayload + class ServerIndex::UnstableResourcePayload { - Orthanc::ResourceType type_; + private: + ResourceType type_; + std::string publicId_; boost::posix_time::ptime time_; - UnstableResourcePayload() : type_(Orthanc::ResourceType_Instance) + public: + UnstableResourcePayload() : type_(ResourceType_Instance) { } - UnstableResourcePayload(Orthanc::ResourceType type) : type_(type) + UnstableResourcePayload(Orthanc::ResourceType type, + const std::string& publicId) : + type_(type), + publicId_(publicId) { time_ = boost::posix_time::second_clock::local_time(); } @@ -207,6 +288,16 @@ { return (boost::posix_time::second_clock::local_time() - time_).total_seconds(); } + + ResourceType GetResourceType() const + { + return type_; + } + + const std::string& GetPublicId() const + { + return publicId_; + } }; @@ -215,19 +306,18 @@ ResourceType expectedType) { boost::mutex::scoped_lock lock(mutex_); - listener_->Reset(); Transaction t(*this); int64_t id; ResourceType type; - if (!db_->LookupResource(uuid, id, type) || + if (!db_.LookupResource(id, type, uuid) || expectedType != type) { return false; } - db_->DeleteResource(id); + db_.DeleteResource(id); if (listener_->HasRemainingLevel()) { @@ -252,18 +342,22 @@ void ServerIndex::FlushThread(ServerIndex* that) { - unsigned int sleep; + // By default, wait for 10 seconds before flushing + unsigned int sleep = 10; try { boost::mutex::scoped_lock lock(that->mutex_); - std::string sleepString = that->db_->GetGlobalProperty(GlobalProperty_FlushSleep); - sleep = boost::lexical_cast<unsigned int>(sleepString); + std::string sleepString; + + if (that->db_.LookupGlobalProperty(sleepString, GlobalProperty_FlushSleep) && + Toolbox::IsInteger(sleepString)) + { + sleep = boost::lexical_cast<unsigned int>(sleepString); + } } catch (boost::bad_lexical_cast&) { - // By default, wait for 10 seconds before flushing - sleep = 10; } LOG(INFO) << "Starting the database flushing thread (sleep = " << sleep << ")"; @@ -280,7 +374,7 @@ } boost::mutex::scoped_lock lock(that->mutex_); - that->db_->FlushToDisk(); + that->db_.FlushToDisk(); count = 0; } @@ -288,7 +382,7 @@ } - static void ComputeExpectedNumberOfInstances(DatabaseWrapper& db, + static void ComputeExpectedNumberOfInstances(IDatabaseWrapper& db, int64_t series, const DicomMap& dicomSummary) { @@ -328,40 +422,147 @@ } + + + bool ServerIndex::GetMetadataAsInteger(int64_t& result, + int64_t id, + MetadataType type) + { + std::string s; + if (!db_.LookupMetadata(s, id, type)) + { + return false; + } + + try + { + result = boost::lexical_cast<int64_t>(s); + return true; + } + catch (boost::bad_lexical_cast&) + { + return false; + } + } + + + void ServerIndex::LogChange(int64_t internalId, + ChangeType changeType, + ResourceType resourceType, + const std::string& publicId) + { + ServerIndexChange change(changeType, resourceType, publicId); + + if (changeType <= ChangeType_INTERNAL_LastLogged) + { + db_.LogChange(internalId, change); + } + + assert(listener_.get() != NULL); + listener_->SignalChange(change); + } + + + uint64_t ServerIndex::IncrementGlobalSequenceInternal(GlobalProperty property) + { + std::string oldValue; + + if (db_.LookupGlobalProperty(oldValue, property)) + { + uint64_t oldNumber; + + try + { + oldNumber = boost::lexical_cast<uint64_t>(oldValue); + db_.SetGlobalProperty(property, boost::lexical_cast<std::string>(oldNumber + 1)); + return oldNumber + 1; + } + catch (boost::bad_lexical_cast&) + { + throw OrthancException(ErrorCode_InternalError); + } + } + else + { + // Initialize the sequence at "1" + db_.SetGlobalProperty(property, "1"); + return 1; + } + } + + + + void ServerIndex::SetMainDicomTags(int64_t resource, + const DicomMap& tags) + { + DicomArray flattened(tags); + for (size_t i = 0; i < flattened.GetSize(); i++) + { + const DicomElement& element = flattened.GetElement(i); + db_.SetMainDicomTag(resource, element.GetTag(), element.GetValue().AsString()); + } + } + + + int64_t ServerIndex::CreateResource(const std::string& publicId, + ResourceType type) + { + int64_t id = db_.CreateResource(publicId, type); + + 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); + } + + ServerIndexChange change(changeType, type, publicId); + db_.LogChange(id, change); + + assert(listener_.get() != NULL); + listener_->SignalChange(change); + + return id; + } + + ServerIndex::ServerIndex(ServerContext& context, - const std::string& dbPath) : + IDatabaseWrapper& db) : done_(false), + db_(db), maximumStorageSize_(0), maximumPatients_(0) { listener_.reset(new Internals::ServerIndexListener(context)); - - if (dbPath == ":memory:") - { - db_.reset(new DatabaseWrapper(*listener_)); - } - else - { - boost::filesystem::path p = dbPath; + db_.SetListener(*listener_); - try - { - boost::filesystem::create_directories(p); - } - catch (boost::filesystem::filesystem_error) - { - } - - db_.reset(new DatabaseWrapper(p.string() + "/index", *listener_)); - } - - currentStorageSize_ = db_->GetTotalCompressedSize(); + currentStorageSize_ = db_.GetTotalCompressedSize(); // Initial recycling if the parameters have changed since the last // execution of Orthanc StandaloneRecycling(); - flushThread_ = boost::thread(FlushThread, this); + if (db.HasFlushToDisk()) + { + flushThread_ = boost::thread(FlushThread, this); + } + unstableResourcesMonitorThread_ = boost::thread(UnstableResourcesMonitorThread, this); } @@ -370,7 +571,8 @@ { done_ = true; - if (flushThread_.joinable()) + if (db_.HasFlushToDisk() && + flushThread_.joinable()) { flushThread_.join(); } @@ -382,12 +584,15 @@ } - StoreStatus ServerIndex::Store(const DicomMap& dicomSummary, + StoreStatus ServerIndex::Store(std::map<MetadataType, std::string>& instanceMetadata, + const DicomMap& dicomSummary, const Attachments& attachments, - const std::string& remoteAet) + const std::string& remoteAet, + const MetadataMap& metadata) { boost::mutex::scoped_lock lock(mutex_); - listener_->Reset(); + + instanceMetadata.clear(); DicomInstanceHasher hasher(dicomSummary); @@ -399,9 +604,10 @@ { ResourceType type; int64_t tmp; - if (db_->LookupResource(hasher.HashInstance(), tmp, type)) + if (db_.LookupResource(tmp, type, hasher.HashInstance())) { assert(type == ResourceType_Instance); + db_.GetAllMetadata(instanceMetadata, tmp); return StoreStatus_AlreadyStored; } } @@ -417,11 +623,11 @@ Recycle(instanceSize, hasher.HashPatient()); // Create the instance - int64_t instance = db_->CreateResource(hasher.HashInstance(), ResourceType_Instance); + int64_t instance = CreateResource(hasher.HashInstance(), ResourceType_Instance); DicomMap dicom; dicomSummary.ExtractInstanceInformation(dicom); - db_->SetMainDicomTags(instance, dicom); + SetMainDicomTags(instance, dicom); // Detect up to which level the patient/study/series/instance // hierarchy must be created @@ -433,26 +639,26 @@ { ResourceType dummy; - if (db_->LookupResource(hasher.HashSeries(), series, dummy)) + if (db_.LookupResource(series, dummy, hasher.HashSeries())) { assert(dummy == ResourceType_Series); // The patient, the study and the series already exist - bool ok = (db_->LookupResource(hasher.HashPatient(), patient, dummy) && - db_->LookupResource(hasher.HashStudy(), study, dummy)); + bool ok = (db_.LookupResource(patient, dummy, hasher.HashPatient()) && + db_.LookupResource(study, dummy, hasher.HashStudy())); assert(ok); } - else if (db_->LookupResource(hasher.HashStudy(), study, dummy)) + else if (db_.LookupResource(study, dummy, hasher.HashStudy())) { assert(dummy == ResourceType_Study); // New series: The patient and the study already exist isNewSeries = true; - bool ok = db_->LookupResource(hasher.HashPatient(), patient, dummy); + bool ok = db_.LookupResource(patient, dummy, hasher.HashPatient()); assert(ok); } - else if (db_->LookupResource(hasher.HashPatient(), patient, dummy)) + else if (db_.LookupResource(patient, dummy, hasher.HashPatient())) { assert(dummy == ResourceType_Patient); @@ -472,38 +678,38 @@ // Create the series if needed if (isNewSeries) { - series = db_->CreateResource(hasher.HashSeries(), ResourceType_Series); + series = CreateResource(hasher.HashSeries(), ResourceType_Series); dicomSummary.ExtractSeriesInformation(dicom); - db_->SetMainDicomTags(series, dicom); + SetMainDicomTags(series, dicom); } // Create the study if needed if (isNewStudy) { - study = db_->CreateResource(hasher.HashStudy(), ResourceType_Study); + study = CreateResource(hasher.HashStudy(), ResourceType_Study); dicomSummary.ExtractStudyInformation(dicom); - db_->SetMainDicomTags(study, dicom); + SetMainDicomTags(study, dicom); } // Create the patient if needed if (isNewPatient) { - patient = db_->CreateResource(hasher.HashPatient(), ResourceType_Patient); + patient = CreateResource(hasher.HashPatient(), ResourceType_Patient); dicomSummary.ExtractPatientInformation(dicom); - db_->SetMainDicomTags(patient, dicom); + SetMainDicomTags(patient, dicom); } // Create the parent-to-child links - db_->AttachChild(series, instance); + db_.AttachChild(series, instance); if (isNewSeries) { - db_->AttachChild(study, series); + db_.AttachChild(study, series); } if (isNewStudy) { - db_->AttachChild(patient, study); + db_.AttachChild(patient, study); } // Sanity checks @@ -516,40 +722,75 @@ for (Attachments::const_iterator it = attachments.begin(); it != attachments.end(); ++it) { - db_->AddAttachment(instance, *it); + db_.AddAttachment(instance, *it); } - // Attach the metadata + // Attach the user-specified metadata + for (MetadataMap::const_iterator + it = metadata.begin(); it != metadata.end(); ++it) + { + switch (it->first.first) + { + case ResourceType_Patient: + db_.SetMetadata(patient, it->first.second, it->second); + break; + + case ResourceType_Study: + db_.SetMetadata(study, it->first.second, it->second); + break; + + case ResourceType_Series: + db_.SetMetadata(series, it->first.second, it->second); + break; + + case ResourceType_Instance: + db_.SetMetadata(instance, it->first.second, it->second); + instanceMetadata[it->first.second] = it->second; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + // Attach the auto-computed metadata for the patient/study/series levels std::string now = Toolbox::GetNowIsoString(); - db_->SetMetadata(instance, MetadataType_Instance_ReceptionDate, now); - db_->SetMetadata(series, MetadataType_LastUpdate, now); - db_->SetMetadata(study, MetadataType_LastUpdate, now); - db_->SetMetadata(patient, MetadataType_LastUpdate, now); - db_->SetMetadata(instance, MetadataType_Instance_RemoteAet, remoteAet); + db_.SetMetadata(series, MetadataType_LastUpdate, now); + db_.SetMetadata(study, MetadataType_LastUpdate, now); + db_.SetMetadata(patient, MetadataType_LastUpdate, now); + + // Attach the auto-computed metadata for the instance level, + // reflecting these additions into the input metadata map + db_.SetMetadata(instance, MetadataType_Instance_ReceptionDate, now); + instanceMetadata[MetadataType_Instance_ReceptionDate] = now; + + db_.SetMetadata(instance, MetadataType_Instance_RemoteAet, remoteAet); + instanceMetadata[MetadataType_Instance_RemoteAet] = remoteAet; const DicomValue* value; if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_INSTANCE_NUMBER)) != NULL || (value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGE_INDEX)) != NULL) { - db_->SetMetadata(instance, MetadataType_Instance_IndexInSeries, value->AsString()); - } - - if (isNewSeries) - { - ComputeExpectedNumberOfInstances(*db_, series, dicomSummary); + db_.SetMetadata(instance, MetadataType_Instance_IndexInSeries, value->AsString()); + instanceMetadata[MetadataType_Instance_IndexInSeries] = value->AsString(); } // Check whether the series of this new instance is now completed + if (isNewSeries) + { + ComputeExpectedNumberOfInstances(db_, series, dicomSummary); + } + SeriesStatus seriesStatus = GetSeriesStatus(series); if (seriesStatus == SeriesStatus_Complete) { - db_->LogChange(ChangeType_CompletedSeries, series, ResourceType_Series); + LogChange(series, ChangeType_CompletedSeries, ResourceType_Series, hasher.HashSeries()); } // Mark the parent resources of this instance as unstable - MarkAsUnstable(patient, ResourceType_Patient); - MarkAsUnstable(study, ResourceType_Study); - MarkAsUnstable(series, ResourceType_Series); + MarkAsUnstable(series, ResourceType_Series, hasher.HashSeries()); + MarkAsUnstable(study, ResourceType_Study, hasher.HashStudy()); + MarkAsUnstable(patient, ResourceType_Patient, hasher.HashPatient()); t.Commit(instanceSize); @@ -557,8 +798,7 @@ } catch (OrthancException& e) { - LOG(ERROR) << "EXCEPTION [" << e.What() << "]" - << " (SQLite status: " << db_->GetErrorMessage() << ")"; + LOG(ERROR) << "EXCEPTION [" << e.What() << "]"; } return StoreStatus_Failure; @@ -571,17 +811,17 @@ target = Json::objectValue; uint64_t cs = currentStorageSize_; - assert(cs == db_->GetTotalCompressedSize()); - uint64_t us = db_->GetTotalUncompressedSize(); + assert(cs == db_.GetTotalCompressedSize()); + uint64_t us = db_.GetTotalUncompressedSize(); target["TotalDiskSize"] = boost::lexical_cast<std::string>(cs); target["TotalUncompressedSize"] = boost::lexical_cast<std::string>(us); - target["TotalDiskSizeMB"] = boost::lexical_cast<unsigned int>(cs / MEGA_BYTES); - target["TotalUncompressedSizeMB"] = boost::lexical_cast<unsigned int>(us / MEGA_BYTES); + target["TotalDiskSizeMB"] = static_cast<unsigned int>(cs / MEGA_BYTES); + target["TotalUncompressedSizeMB"] = static_cast<unsigned int>(us / MEGA_BYTES); - 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)); + 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)); } @@ -589,34 +829,23 @@ SeriesStatus ServerIndex::GetSeriesStatus(int64_t id) { // 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); - } - catch (boost::bad_lexical_cast&) + int64_t expected; + if (!GetMetadataAsInteger(expected, id, MetadataType_Series_ExpectedNumberOfInstances)) { return SeriesStatus_Unknown; } // Loop over the instances of this series std::list<int64_t> children; - db_->GetChildrenInternalId(children, id); + db_.GetChildrenInternalId(children, id); - std::set<size_t> instances; + std::set<int64_t> instances; for (std::list<int64_t>::const_iterator it = children.begin(); it != children.end(); ++it) { // 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&) + int64_t index; + if (!GetMetadataAsInteger(index, *it, MetadataType_Instance_IndexInSeries)) { return SeriesStatus_Unknown; } @@ -636,7 +865,7 @@ instances.insert(index); } - if (instances.size() == expected) + if (static_cast<int64_t>(instances.size()) == expected) { return SeriesStatus_Complete; } @@ -652,7 +881,7 @@ int64_t resourceId) { DicomMap tags; - db_->GetMainDicomTags(tags, resourceId); + db_.GetMainDicomTags(tags, resourceId); target["MainDicomTags"] = Json::objectValue; FromDcmtkBridge::ToJson(target["MainDicomTags"], tags); } @@ -668,7 +897,7 @@ // Lookup for the requested resource int64_t id; ResourceType type; - if (!db_->LookupResource(publicId, id, type) || + if (!db_.LookupResource(id, type, publicId) || type != expectedType) { return false; @@ -678,12 +907,12 @@ if (type != ResourceType_Patient) { int64_t parentId; - if (!db_->LookupParent(parentId, id)) + if (!db_.LookupParent(parentId, id)) { throw OrthancException(ErrorCode_InternalError); } - std::string parent = db_->GetPublicId(parentId); + std::string parent = db_.GetPublicId(parentId); switch (type) { @@ -706,7 +935,7 @@ // List the children resources std::list<std::string> children; - db_->GetChildrenPublicId(children, id); + db_.GetChildrenPublicId(children, id); if (type != ResourceType_Instance) { @@ -753,9 +982,9 @@ result["Type"] = "Series"; result["Status"] = EnumerationToString(GetSeriesStatus(id)); - int i; - if (db_->GetMetadataAsInteger(i, id, MetadataType_Series_ExpectedNumberOfInstances)) - result["ExpectedNumberOfInstances"] = i; + int64_t i; + if (GetMetadataAsInteger(i, id, MetadataType_Series_ExpectedNumberOfInstances)) + result["ExpectedNumberOfInstances"] = static_cast<int>(i); else result["ExpectedNumberOfInstances"] = Json::nullValue; @@ -767,7 +996,7 @@ result["Type"] = "Instance"; FileInfo attachment; - if (!db_->LookupAttachment(attachment, id, FileContentType_Dicom)) + if (!db_.LookupAttachment(attachment, id, FileContentType_Dicom)) { throw OrthancException(ErrorCode_InternalError); } @@ -775,9 +1004,9 @@ result["FileSize"] = static_cast<unsigned int>(attachment.GetUncompressedSize()); result["FileUuid"] = attachment.GetUuid(); - int i; - if (db_->GetMetadataAsInteger(i, id, MetadataType_Instance_IndexInSeries)) - result["IndexInSeries"] = i; + int64_t i; + if (GetMetadataAsInteger(i, id, MetadataType_Instance_IndexInSeries)) + result["IndexInSeries"] = static_cast<int>(i); else result["IndexInSeries"] = Json::nullValue; @@ -794,19 +1023,26 @@ std::string tmp; - tmp = db_->GetMetadata(id, MetadataType_AnonymizedFrom); - if (tmp.size() != 0) + if (db_.LookupMetadata(tmp, id, MetadataType_AnonymizedFrom)) + { result["AnonymizedFrom"] = tmp; + } - tmp = db_->GetMetadata(id, MetadataType_ModifiedFrom); - if (tmp.size() != 0) + if (db_.LookupMetadata(tmp, id, MetadataType_ModifiedFrom)) + { result["ModifiedFrom"] = tmp; + } if (type == ResourceType_Patient || type == ResourceType_Study || type == ResourceType_Series) { result["IsStable"] = !unstableResources_.Contains(id); + + if (db_.LookupMetadata(tmp, id, MetadataType_LastUpdate)) + { + result["LastUpdate"] = tmp; + } } return true; @@ -821,12 +1057,12 @@ int64_t id; ResourceType type; - if (!db_->LookupResource(instanceUuid, id, type)) + if (!db_.LookupResource(id, type, instanceUuid)) { - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_UnknownResource); } - if (db_->LookupAttachment(attachment, id, contentType)) + if (db_.LookupAttachment(attachment, id, contentType)) { assert(attachment.GetContentType() == contentType); return true; @@ -839,38 +1075,77 @@ - void ServerIndex::GetAllUuids(Json::Value& target, + void ServerIndex::GetAllUuids(std::list<std::string>& target, ResourceType resourceType) { boost::mutex::scoped_lock lock(mutex_); - db_->GetAllPublicIds(target, resourceType); + db_.GetAllPublicIds(target, resourceType); } - bool ServerIndex::GetChanges(Json::Value& target, + template <typename T> + static void FormatLog(Json::Value& target, + const std::list<T>& log, + const std::string& name, + bool done, + int64_t since) + { + Json::Value items = Json::arrayValue; + for (typename std::list<T>::const_iterator + it = log.begin(); it != log.end(); ++it) + { + Json::Value item; + it->Format(item); + items.append(item); + } + + target = Json::objectValue; + target[name] = items; + target["Done"] = done; + + int64_t last = (log.empty() ? since : log.back().GetSeq()); + target["Last"] = static_cast<int>(last); + } + + + void ServerIndex::GetChanges(Json::Value& target, int64_t since, unsigned int maxResults) { - boost::mutex::scoped_lock lock(mutex_); - db_->GetChanges(target, since, maxResults); - return true; + std::list<ServerIndexChange> changes; + bool done; + + { + boost::mutex::scoped_lock lock(mutex_); + db_.GetChanges(changes, done, since, maxResults); + } + + FormatLog(target, changes, "Changes", done, since); } - bool ServerIndex::GetLastChange(Json::Value& target) + + void ServerIndex::GetLastChange(Json::Value& target) { - boost::mutex::scoped_lock lock(mutex_); - db_->GetLastChange(target); - return true; + std::list<ServerIndexChange> changes; + + { + boost::mutex::scoped_lock lock(mutex_); + db_.GetLastChange(changes); + } + + FormatLog(target, changes, "Changes", true, 0); } + void ServerIndex::LogExportedResource(const std::string& publicId, const std::string& remoteModality) { boost::mutex::scoped_lock lock(mutex_); + Transaction transaction(*this); int64_t id; ResourceType type; - if (!db_->LookupResource(publicId, id, type)) + if (!db_.LookupResource(id, type, publicId)) { throw OrthancException(ErrorCode_InternalError); } @@ -888,7 +1163,7 @@ while (!done) { DicomMap map; - db_->GetMainDicomTags(map, currentId); + db_.GetMainDicomTags(map, currentId); switch (currentType) { @@ -920,36 +1195,52 @@ // the current resource if (!done) { - bool ok = db_->LookupParent(currentId, currentId); + bool ok = db_.LookupParent(currentId, currentId); assert(ok); } } - // No need for a SQLite::Transaction here, as we only insert 1 record - db_->LogExportedResource(type, - publicId, - remoteModality, - patientId, - studyInstanceUid, - seriesInstanceUid, - sopInstanceUid); + ExportedResource resource(-1, + type, + publicId, + remoteModality, + Toolbox::GetNowIsoString(), + patientId, + studyInstanceUid, + seriesInstanceUid, + sopInstanceUid); + + db_.LogExportedResource(resource); + transaction.Commit(0); } - bool ServerIndex::GetExportedResources(Json::Value& target, + void ServerIndex::GetExportedResources(Json::Value& target, int64_t since, unsigned int maxResults) { - boost::mutex::scoped_lock lock(mutex_); - db_->GetExportedResources(target, since, maxResults); - return true; + std::list<ExportedResource> exported; + bool done; + + { + boost::mutex::scoped_lock lock(mutex_); + db_.GetExportedResources(exported, done, since, maxResults); + } + + FormatLog(target, exported, "Exports", done, since); } - bool ServerIndex::GetLastExportedResource(Json::Value& target) + + void ServerIndex::GetLastExportedResource(Json::Value& target) { - boost::mutex::scoped_lock lock(mutex_); - db_->GetLastExportedResource(target); - return true; + std::list<ExportedResource> exported; + + { + boost::mutex::scoped_lock lock(mutex_); + db_.GetLastExportedResource(exported); + } + + FormatLog(target, exported, "Exports", true, 0); } @@ -958,7 +1249,7 @@ if (maximumStorageSize_ != 0) { uint64_t currentSize = currentStorageSize_ - listener_->GetSizeOfFilesToRemove(); - assert(db_->GetTotalCompressedSize() == currentSize); + assert(db_.GetTotalCompressedSize() == currentSize); if (currentSize + instanceSize > maximumStorageSize_) { @@ -968,7 +1259,7 @@ if (maximumPatients_ != 0) { - uint64_t patientCount = db_->GetResourceCount(ResourceType_Patient); + uint64_t patientCount = db_.GetResourceCount(ResourceType_Patient); if (patientCount > maximumPatients_) { return true; @@ -991,7 +1282,7 @@ // already stored int64_t patientToAvoid; ResourceType type; - bool hasPatientToAvoid = db_->LookupResource(newPatientId, patientToAvoid, type); + bool hasPatientToAvoid = db_.LookupResource(patientToAvoid, type, newPatientId); if (hasPatientToAvoid && type != ResourceType_Patient) { @@ -1006,16 +1297,16 @@ // 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); + db_.SelectPatientToRecycle(patientToRecycle, patientToAvoid) : + db_.SelectPatientToRecycle(patientToRecycle); if (!ok) { throw OrthancException(ErrorCode_FullStorage); } - LOG(INFO) << "Recycling one patient"; - db_->DeleteResource(patientToRecycle); + VLOG(1) << "Recycling one patient"; + db_.DeleteResource(patientToRecycle); if (!IsRecyclingNeeded(instanceSize)) { @@ -1075,13 +1366,13 @@ // Lookup for the requested resource int64_t id; ResourceType type; - if (!db_->LookupResource(publicId, id, type) || + if (!db_.LookupResource(id, type, publicId) || type != ResourceType_Patient) { throw OrthancException(ErrorCode_ParameterOutOfRange); } - return db_->IsProtectedPatient(id); + return db_.IsProtectedPatient(id); } @@ -1089,18 +1380,19 @@ bool isProtected) { boost::mutex::scoped_lock lock(mutex_); + Transaction transaction(*this); // Lookup for the requested resource int64_t id; ResourceType type; - if (!db_->LookupResource(publicId, id, type) || + if (!db_.LookupResource(id, type, publicId) || 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); + db_.SetProtectedPatient(id, isProtected); + transaction.Commit(0); if (isProtected) LOG(INFO) << "Patient " << publicId << " has been protected"; @@ -1118,7 +1410,7 @@ ResourceType type; int64_t resource; - if (!db_->LookupResource(publicId, resource, type)) + if (!db_.LookupResource(resource, type, publicId)) { throw OrthancException(ErrorCode_UnknownResource); } @@ -1130,12 +1422,12 @@ } std::list<int64_t> tmp; - db_->GetChildrenInternalId(tmp, resource); + db_.GetChildrenInternalId(tmp, resource); for (std::list<int64_t>::const_iterator it = tmp.begin(); it != tmp.end(); ++it) { - result.push_back(db_->GetPublicId(*it)); + result.push_back(db_.GetPublicId(*it)); } } @@ -1149,7 +1441,7 @@ ResourceType type; int64_t top; - if (!db_->LookupResource(publicId, top, type)) + if (!db_.LookupResource(top, type, publicId)) { throw OrthancException(ErrorCode_UnknownResource); } @@ -1172,14 +1464,14 @@ int64_t resource = toExplore.top(); toExplore.pop(); - if (db_->GetResourceType(resource) == ResourceType_Instance) + if (db_.GetResourceType(resource) == ResourceType_Instance) { - result.push_back(db_->GetPublicId(resource)); + result.push_back(db_.GetPublicId(resource)); } else { // Tag all the children of this resource as to be explored - db_->GetChildrenInternalId(tmp, resource); + db_.GetChildrenInternalId(tmp, resource); for (std::list<int64_t>::const_iterator it = tmp.begin(); it != tmp.end(); ++it) { @@ -1198,12 +1490,12 @@ ResourceType rtype; int64_t id; - if (!db_->LookupResource(publicId, id, rtype)) + if (!db_.LookupResource(id, rtype, publicId)) { throw OrthancException(ErrorCode_UnknownResource); } - db_->SetMetadata(id, type, value); + db_.SetMetadata(id, type, value); } @@ -1214,12 +1506,12 @@ ResourceType rtype; int64_t id; - if (!db_->LookupResource(publicId, id, rtype)) + if (!db_.LookupResource(id, rtype, publicId)) { throw OrthancException(ErrorCode_UnknownResource); } - db_->DeleteMetadata(id, type); + db_.DeleteMetadata(id, type); } @@ -1231,12 +1523,12 @@ ResourceType rtype; int64_t id; - if (!db_->LookupResource(publicId, id, rtype)) + if (!db_.LookupResource(id, rtype, publicId)) { throw OrthancException(ErrorCode_UnknownResource); } - return db_->LookupMetadata(target, id, type); + return db_.LookupMetadata(target, id, type); } @@ -1247,12 +1539,12 @@ ResourceType rtype; int64_t id; - if (!db_->LookupResource(publicId, id, rtype)) + if (!db_.LookupResource(id, rtype, publicId)) { throw OrthancException(ErrorCode_UnknownResource); } - db_->ListAvailableMetadata(target, id); + db_.ListAvailableMetadata(target, id); } @@ -1264,13 +1556,13 @@ ResourceType type; int64_t id; - if (!db_->LookupResource(publicId, id, type) || + if (!db_.LookupResource(id, type, publicId) || expectedType != type) { throw OrthancException(ErrorCode_UnknownResource); } - db_->ListAvailableAttachments(target, id); + db_.ListAvailableAttachments(target, id); } @@ -1281,15 +1573,15 @@ ResourceType type; int64_t id; - if (!db_->LookupResource(publicId, id, type)) + if (!db_.LookupResource(id, type, publicId)) { throw OrthancException(ErrorCode_UnknownResource); } int64_t parentId; - if (db_->LookupParent(parentId, id)) + if (db_.LookupParent(parentId, id)) { - target = db_->GetPublicId(parentId); + target = db_.GetPublicId(parentId); return true; } else @@ -1302,12 +1594,10 @@ uint64_t ServerIndex::IncrementGlobalSequence(GlobalProperty sequence) { boost::mutex::scoped_lock lock(mutex_); - - std::auto_ptr<SQLite::Transaction> transaction(db_->StartTransaction()); + Transaction transaction(*this); - transaction->Begin(); - uint64_t seq = db_->IncrementGlobalSequence(sequence); - transaction->Commit(); + uint64_t seq = IncrementGlobalSequenceInternal(sequence); + transaction.Commit(0); return seq; } @@ -1318,32 +1608,30 @@ const std::string& publicId) { boost::mutex::scoped_lock lock(mutex_); - std::auto_ptr<SQLite::Transaction> transaction(db_->StartTransaction()); - transaction->Begin(); + Transaction transaction(*this); int64_t id; ResourceType type; - if (!db_->LookupResource(publicId, id, type)) + if (!db_.LookupResource(id, type, publicId)) { throw OrthancException(ErrorCode_UnknownResource); } - db_->LogChange(changeType, id, type); - - transaction->Commit(); + LogChange(id, changeType, type, publicId); + transaction.Commit(0); } void ServerIndex::DeleteChanges() { boost::mutex::scoped_lock lock(mutex_); - db_->ClearTable("Changes"); + db_.ClearChanges(); } void ServerIndex::DeleteExportedResources() { boost::mutex::scoped_lock lock(mutex_); - db_->ClearTable("ExportedResources"); + db_.ClearExportedResources(); } @@ -1370,16 +1658,16 @@ int64_t resource = toExplore.top(); toExplore.pop(); - ResourceType thisType = db_->GetResourceType(resource); + ResourceType thisType = db_.GetResourceType(resource); std::list<FileContentType> f; - db_->ListAvailableAttachments(f, resource); + db_.ListAvailableAttachments(f, resource); for (std::list<FileContentType>::const_iterator it = f.begin(); it != f.end(); ++it) { FileInfo attachment; - if (db_->LookupAttachment(attachment, resource, *it)) + if (db_.LookupAttachment(attachment, resource, *it)) { compressedSize += attachment.GetCompressedSize(); uncompressedSize += attachment.GetUncompressedSize(); @@ -1408,7 +1696,7 @@ // Tag all the children of this resource as to be explored std::list<int64_t> tmp; - db_->GetChildrenInternalId(tmp, resource); + db_.GetChildrenInternalId(tmp, resource); for (std::list<int64_t>::const_iterator it = tmp.begin(); it != tmp.end(); ++it) { @@ -1437,7 +1725,7 @@ ResourceType type; int64_t top; - if (!db_->LookupResource(publicId, top, type)) + if (!db_.LookupResource(top, type, publicId)) { throw OrthancException(ErrorCode_UnknownResource); } @@ -1452,9 +1740,9 @@ target = Json::objectValue; target["DiskSize"] = boost::lexical_cast<std::string>(compressedSize); - target["DiskSizeMB"] = boost::lexical_cast<unsigned int>(compressedSize / MEGA_BYTES); + target["DiskSizeMB"] = static_cast<unsigned int>(compressedSize / MEGA_BYTES); target["UncompressedSize"] = boost::lexical_cast<std::string>(uncompressedSize); - target["UncompressedSizeMB"] = boost::lexical_cast<unsigned int>(uncompressedSize / MEGA_BYTES); + target["UncompressedSizeMB"] = static_cast<unsigned int>(uncompressedSize / MEGA_BYTES); switch (type) { @@ -1486,7 +1774,7 @@ ResourceType type; int64_t top; - if (!db_->LookupResource(publicId, top, type)) + if (!db_.LookupResource(top, type, publicId)) { throw OrthancException(ErrorCode_UnknownResource); } @@ -1523,20 +1811,20 @@ int64_t id = that->unstableResources_.RemoveOldest(payload); // Ensure that the resource is still existing before logging the change - if (that->db_->IsExistingResource(id)) + if (that->db_.IsExistingResource(id)) { - switch (payload.type_) + switch (payload.GetResourceType()) { - case Orthanc::ResourceType_Patient: - that->db_->LogChange(ChangeType_StablePatient, id, ResourceType_Patient); + case ResourceType_Patient: + that->LogChange(id, ChangeType_StablePatient, ResourceType_Patient, payload.GetPublicId()); break; - case Orthanc::ResourceType_Study: - that->db_->LogChange(ChangeType_StableStudy, id, ResourceType_Study); + case ResourceType_Study: + that->LogChange(id, ChangeType_StableStudy, ResourceType_Study, payload.GetPublicId()); break; - case Orthanc::ResourceType_Series: - that->db_->LogChange(ChangeType_StableSeries, id, ResourceType_Series); + case ResourceType_Series: + that->LogChange(id, ChangeType_StableSeries, ResourceType_Series, payload.GetPublicId()); break; default: @@ -1553,7 +1841,8 @@ void ServerIndex::MarkAsUnstable(int64_t id, - Orthanc::ResourceType type) + Orthanc::ResourceType type, + const std::string& publicId) { // WARNING: Before calling this method, "mutex_" must be locked. @@ -1561,68 +1850,72 @@ type == Orthanc::ResourceType_Study || type == Orthanc::ResourceType_Series); - unstableResources_.AddOrMakeMostRecent(id, type); + UnstableResourcePayload payload(type, publicId); + unstableResources_.AddOrMakeMostRecent(id, payload); //LOG(INFO) << "Unstable resource: " << EnumerationToString(type) << " " << id; + + LogChange(id, ChangeType_NewChildInstance, type, publicId); } - void ServerIndex::LookupTagValue(std::list<std::string>& result, - DicomTag tag, - const std::string& value, - ResourceType type) + void ServerIndex::LookupIdentifier(std::list<std::string>& result, + const DicomTag& tag, + const std::string& value, + ResourceType type) { result.clear(); boost::mutex::scoped_lock lock(mutex_); std::list<int64_t> id; - db_->LookupTagValue(id, tag, value); + db_.LookupIdentifier(id, tag, value); for (std::list<int64_t>::const_iterator it = id.begin(); it != id.end(); ++it) { - if (db_->GetResourceType(*it) == type) + if (db_.GetResourceType(*it) == type) { - result.push_back(db_->GetPublicId(*it)); + result.push_back(db_.GetPublicId(*it)); } } } - void ServerIndex::LookupTagValue(std::list<std::string>& result, - DicomTag tag, - const std::string& value) + void ServerIndex::LookupIdentifier(std::list<std::string>& result, + const DicomTag& tag, + const std::string& value) { result.clear(); boost::mutex::scoped_lock lock(mutex_); std::list<int64_t> id; - db_->LookupTagValue(id, tag, value); + db_.LookupIdentifier(id, tag, value); for (std::list<int64_t>::const_iterator it = id.begin(); it != id.end(); ++it) { - result.push_back(db_->GetPublicId(*it)); + result.push_back(db_.GetPublicId(*it)); } } - void ServerIndex::LookupTagValue(std::list<std::string>& result, - const std::string& value) + void ServerIndex::LookupIdentifier(std::list< std::pair<ResourceType, std::string> >& result, + const std::string& value) { result.clear(); boost::mutex::scoped_lock lock(mutex_); std::list<int64_t> id; - db_->LookupTagValue(id, value); + db_.LookupIdentifier(id, value); for (std::list<int64_t>::const_iterator it = id.begin(); it != id.end(); ++it) { - result.push_back(db_->GetPublicId(*it)); + result.push_back(std::make_pair(db_.GetResourceType(*it), + db_.GetPublicId(*it))); } } @@ -1636,20 +1929,20 @@ ResourceType resourceType; int64_t resourceId; - if (!db_->LookupResource(publicId, resourceId, resourceType)) + if (!db_.LookupResource(resourceId, resourceType, publicId)) { return StoreStatus_Failure; // Inexistent resource } // Remove possible previous attachment - db_->DeleteAttachment(resourceId, attachment.GetContentType()); + db_.DeleteAttachment(resourceId, attachment.GetContentType()); // Locate the patient of the target resource int64_t patientId = resourceId; for (;;) { int64_t parent; - if (db_->LookupParent(parent, patientId)) + if (db_.LookupParent(parent, patientId)) { // We have not reached the patient level yet patientId = parent; @@ -1662,10 +1955,10 @@ } // Possibly apply the recycling mechanism while preserving this patient - assert(db_->GetResourceType(patientId) == ResourceType_Patient); - Recycle(attachment.GetCompressedSize(), db_->GetPublicId(patientId)); + assert(db_.GetResourceType(patientId) == ResourceType_Patient); + Recycle(attachment.GetCompressedSize(), db_.GetPublicId(patientId)); - db_->AddAttachment(resourceId, attachment); + db_.AddAttachment(resourceId, attachment); t.Commit(attachment.GetCompressedSize()); @@ -1677,21 +1970,101 @@ FileContentType type) { boost::mutex::scoped_lock lock(mutex_); - listener_->Reset(); - Transaction t(*this); ResourceType rtype; int64_t id; - if (!db_->LookupResource(publicId, id, rtype)) + if (!db_.LookupResource(id, rtype, publicId)) { throw OrthancException(ErrorCode_UnknownResource); } - db_->DeleteAttachment(id, type); + db_.DeleteAttachment(id, type); t.Commit(0); } + bool ServerIndex::GetMetadata(Json::Value& target, + const std::string& publicId) + { + boost::mutex::scoped_lock lock(mutex_); + + target = Json::objectValue; + + ResourceType type; + int64_t id; + if (!db_.LookupResource(id, type, publicId)) + { + return false; + } + + std::list<MetadataType> metadata; + db_.ListAvailableMetadata(metadata, id); + + for (std::list<MetadataType>::const_iterator + it = metadata.begin(); it != metadata.end(); ++it) + { + std::string key = EnumerationToString(*it); + + std::string value; + if (!db_.LookupMetadata(value, id, *it)) + { + value.clear(); + } + + target[key] = value; + } + + return true; + } + + + void ServerIndex::SetGlobalProperty(GlobalProperty property, + const std::string& value) + { + boost::mutex::scoped_lock lock(mutex_); + db_.SetGlobalProperty(property, value); + } + + + std::string ServerIndex::GetGlobalProperty(GlobalProperty property, + const std::string& defaultValue) + { + boost::mutex::scoped_lock lock(mutex_); + + std::string value; + if (db_.LookupGlobalProperty(value, property)) + { + return value; + } + else + { + return defaultValue; + } + } + + + bool ServerIndex::GetMainDicomTags(DicomMap& result, + const std::string& publicId, + ResourceType expectedType) + { + result.Clear(); + + boost::mutex::scoped_lock lock(mutex_); + + // Lookup for the requested resource + int64_t id; + ResourceType type; + if (!db_.LookupResource(id, type, publicId) || + type != expectedType) + { + return false; + } + else + { + db_.GetMainDicomTags(result, id); + return true; + } + } }
--- a/OrthancServer/ServerIndex.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/ServerIndex.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -40,7 +40,7 @@ #include "../Core/DicomFormat/DicomInstanceHasher.h" #include "ServerEnumerations.h" -#include "DatabaseWrapper.h" +#include "IDatabaseWrapper.h" namespace Orthanc @@ -54,9 +54,13 @@ class ServerIndex : public boost::noncopyable { + public: + typedef std::list<FileInfo> Attachments; + typedef std::map< std::pair<ResourceType, MetadataType>, std::string> MetadataMap; + private: class Transaction; - struct UnstableResourcePayload; + class UnstableResourcePayload; bool done_; boost::mutex mutex_; @@ -64,7 +68,7 @@ boost::thread unstableResourcesMonitorThread_; std::auto_ptr<Internals::ServerIndexListener> listener_; - std::auto_ptr<DatabaseWrapper> db_; + IDatabaseWrapper& db_; LeastRecentlyUsedIndex<int64_t, UnstableResourcePayload> unstableResources_; uint64_t currentStorageSize_; @@ -88,7 +92,8 @@ void StandaloneRecycling(); void MarkAsUnstable(int64_t id, - Orthanc::ResourceType type); + Orthanc::ResourceType type, + const std::string& publicId); void GetStatisticsInternal(/* out */ uint64_t& compressedSize, /* out */ uint64_t& uncompressedSize, @@ -98,11 +103,26 @@ /* in */ int64_t id, /* in */ ResourceType type); - public: - typedef std::list<FileInfo> Attachments; + bool GetMetadataAsInteger(int64_t& result, + int64_t id, + MetadataType type); + + void LogChange(int64_t internalId, + ChangeType changeType, + ResourceType resourceType, + const std::string& publicId); + uint64_t IncrementGlobalSequenceInternal(GlobalProperty property); + + void SetMainDicomTags(int64_t resource, + const DicomMap& tags); + + int64_t CreateResource(const std::string& publicId, + ResourceType type); + + public: ServerIndex(ServerContext& context, - const std::string& dbPath); + IDatabaseWrapper& database); ~ServerIndex(); @@ -122,9 +142,11 @@ // "count == 0" means no limit on the number of patients void SetMaximumPatientCount(unsigned int count); - StoreStatus Store(const DicomMap& dicomSummary, + StoreStatus Store(std::map<MetadataType, std::string>& instanceMetadata, + const DicomMap& dicomSummary, const Attachments& attachments, - const std::string& remoteAet); + const std::string& remoteAet, + const MetadataMap& metadata); void ComputeStatistics(Json::Value& target); @@ -136,27 +158,27 @@ const std::string& instanceUuid, FileContentType contentType); - void GetAllUuids(Json::Value& target, + void GetAllUuids(std::list<std::string>& target, ResourceType resourceType); - bool DeleteResource(Json::Value& target, + bool DeleteResource(Json::Value& target /* out */, const std::string& uuid, ResourceType expectedType); - bool GetChanges(Json::Value& target, + void GetChanges(Json::Value& target, int64_t since, unsigned int maxResults); - bool GetLastChange(Json::Value& target); + void GetLastChange(Json::Value& target); void LogExportedResource(const std::string& publicId, const std::string& remoteModality); - bool GetExportedResources(Json::Value& target, + void GetExportedResources(Json::Value& target, int64_t since, unsigned int maxResults); - bool GetLastExportedResource(Json::Value& target); + void GetLastExportedResource(Json::Value& target); bool IsProtectedPatient(const std::string& publicId); @@ -183,6 +205,9 @@ void ListAvailableMetadata(std::list<MetadataType>& target, const std::string& publicId); + bool GetMetadata(Json::Value& target, + const std::string& publicId); + void ListAvailableAttachments(std::list<FileContentType>& target, const std::string& publicId, ResourceType expectedType); @@ -209,22 +234,32 @@ /* out */ unsigned int& countInstances, const std::string& publicId); - void LookupTagValue(std::list<std::string>& result, - DicomTag tag, - const std::string& value, - ResourceType type); + void LookupIdentifier(std::list<std::string>& result, + const DicomTag& tag, + const std::string& value, + ResourceType type); - void LookupTagValue(std::list<std::string>& result, - DicomTag tag, - const std::string& value); + void LookupIdentifier(std::list<std::string>& result, + const DicomTag& tag, + const std::string& value); - void LookupTagValue(std::list<std::string>& result, - const std::string& value); + void LookupIdentifier(std::list< std::pair<ResourceType, std::string> >& result, + const std::string& value); StoreStatus AddAttachment(const FileInfo& attachment, const std::string& publicId); void DeleteAttachment(const std::string& publicId, FileContentType type); + + void SetGlobalProperty(GlobalProperty property, + const std::string& value); + + std::string GetGlobalProperty(GlobalProperty property, + const std::string& defaultValue); + + bool GetMainDicomTags(DicomMap& result, + const std::string& publicId, + ResourceType expectedType); }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerIndexChange.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,113 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "ServerEnumerations.h" +#include "../Core/Toolbox.h" + +#include <string> +#include <json/value.h> + +namespace Orthanc +{ + struct ServerIndexChange + { + private: + int64_t seq_; + ChangeType changeType_; + ResourceType resourceType_; + std::string publicId_; + std::string date_; + + public: + ServerIndexChange(ChangeType changeType, + ResourceType resourceType, + const std::string& publicId) : + seq_(-1), + changeType_(changeType), + resourceType_(resourceType), + publicId_(publicId), + date_(Toolbox::GetNowIsoString()) + { + } + + ServerIndexChange(int64_t seq, + ChangeType changeType, + ResourceType resourceType, + const std::string& publicId, + const std::string& date) : + seq_(seq), + changeType_(changeType), + resourceType_(resourceType), + publicId_(publicId), + date_(date) + { + } + + int64_t GetSeq() const + { + return seq_; + } + + ChangeType GetChangeType() const + { + return changeType_; + } + + ResourceType GetResourceType() const + { + return resourceType_; + } + + const std::string& GetPublicId() const + { + return publicId_; + } + + const std::string& GetDate() const + { + return date_; + } + + void Format(Json::Value& item) const + { + item = Json::objectValue; + item["Seq"] = static_cast<int>(seq_); + item["ChangeType"] = EnumerationToString(changeType_); + item["ResourceType"] = EnumerationToString(resourceType_); + item["ID"] = publicId_; + item["Path"] = GetBasePath(resourceType_, publicId_); + item["Date"] = date_; + } + }; +}
--- a/OrthancServer/ServerToolbox.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/ServerToolbox.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancServer/ServerToolbox.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/ServerToolbox.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancServer/ToDcmtkBridge.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/ToDcmtkBridge.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/OrthancServer/ToDcmtkBridge.h Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/ToDcmtkBridge.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Upgrade4To5.sql Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,66 @@ +-- This SQLite script updates the version of the Orthanc database from 4 to 5. + + +-- Remove 2 indexes to speed up + +DROP INDEX MainDicomTagsIndex2; +DROP INDEX MainDicomTagsIndexValues; + + +-- Add a new table to index the DICOM identifiers + +CREATE TABLE DicomIdentifiers( + id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, + tagGroup INTEGER, + tagElement INTEGER, + value TEXT, + PRIMARY KEY(id, tagGroup, tagElement) + ); + +CREATE INDEX DicomIdentifiersIndex1 ON DicomIdentifiers(id); +CREATE INDEX DicomIdentifiersIndex2 ON DicomIdentifiers(tagGroup, tagElement); +CREATE INDEX DicomIdentifiersIndexValues ON DicomIdentifiers(value COLLATE BINARY); + + +-- Migrate data from MainDicomTags to MainResourcesTags and MainInstancesTags + +INSERT INTO DicomIdentifiers SELECT * FROM MainDicomTags + WHERE ((tagGroup = 16 AND tagElement = 32) OR -- PatientID (0x0010, 0x0020) + (tagGroup = 32 AND tagElement = 13) OR -- StudyInstanceUID (0x0020, 0x000d) + (tagGroup = 8 AND tagElement = 80) OR -- AccessionNumber (0x0008, 0x0050) + (tagGroup = 32 AND tagElement = 14) OR -- SeriesInstanceUID (0x0020, 0x000e) + (tagGroup = 8 AND tagElement = 24)); -- SOPInstanceUID (0x0008, 0x0018) + +DELETE FROM MainDicomTags + WHERE ((tagGroup = 16 AND tagElement = 32) OR -- PatientID (0x0010, 0x0020) + (tagGroup = 32 AND tagElement = 13) OR -- StudyInstanceUID (0x0020, 0x000d) + (tagGroup = 8 AND tagElement = 80) OR -- AccessionNumber (0x0008, 0x0050) + (tagGroup = 32 AND tagElement = 14) OR -- SeriesInstanceUID (0x0020, 0x000e) + (tagGroup = 8 AND tagElement = 24)); -- SOPInstanceUID (0x0008, 0x0018) + + +-- Upgrade the "ResourceDeleted" trigger + +DROP TRIGGER ResourceDeleted; +DROP TRIGGER ResourceDeletedParentCleaning; + +CREATE TRIGGER ResourceDeleted +AFTER DELETE ON Resources +BEGIN + SELECT SignalResourceDeleted(old.publicId, old.resourceType); + SELECT SignalRemainingAncestor(parent.publicId, parent.resourceType) + FROM Resources AS parent WHERE internalId = old.parentId; +END; + +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; + + +-- Change the database version +-- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration + +UPDATE GlobalProperties SET value="5" WHERE property=1;
--- a/OrthancServer/main.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/OrthancServer/main.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -37,6 +37,7 @@ #include <glog/logging.h> #include <boost/algorithm/string/predicate.hpp> +#include "../Core/Uuid.h" #include "../Core/HttpServer/EmbeddedResourceHttpHandler.h" #include "../Core/HttpServer/FilesystemHttpHandler.h" #include "../Core/Lua/LuaFunctionCall.h" @@ -48,10 +49,14 @@ #include "OrthancFindRequestHandler.h" #include "OrthancMoveRequestHandler.h" #include "ServerToolbox.h" +#include "../Plugins/Engine/PluginsManager.h" +#include "../Plugins/Engine/OrthancPlugins.h" using namespace Orthanc; +#define ENABLE_PLUGINS 1 + class OrthancStoreRequestHandler : public IStoreRequestHandler { @@ -67,11 +72,20 @@ virtual void Handle(const std::string& dicomFile, const DicomMap& dicomSummary, const Json::Value& dicomJson, - const std::string& remoteAet) + const std::string& remoteAet, + const std::string& calledAet) { if (dicomFile.size() > 0) { - server_.Store(&dicomFile[0], dicomFile.size(), dicomSummary, dicomJson, remoteAet); + DicomInstanceToStore toStore; + toStore.SetBuffer(dicomFile); + toStore.SetSummary(dicomSummary); + toStore.SetJson(dicomJson); + toStore.SetRemoteAet(remoteAet); + toStore.SetCalledAet(calledAet); + + std::string id; + server_.Store(id, toStore); } } }; @@ -139,7 +153,14 @@ class OrthancApplicationEntityFilter : public IApplicationEntityFilter { +private: + ServerContext& context_; + public: + OrthancApplicationEntityFilter(ServerContext& context) : context_(context) + { + } + virtual bool IsAllowedConnection(const std::string& /*callingIp*/, const std::string& /*callingAet*/) { @@ -166,6 +187,63 @@ return true; } } + + virtual bool IsAllowedTransferSyntax(const std::string& callingIp, + const std::string& callingAet, + TransferSyntax syntax) + { + std::string configuration; + + switch (syntax) + { + case TransferSyntax_Deflated: + configuration = "DeflatedTransferSyntaxAccepted"; + break; + + case TransferSyntax_Jpeg: + configuration = "JpegTransferSyntaxAccepted"; + break; + + case TransferSyntax_Jpeg2000: + configuration = "Jpeg2000TransferSyntaxAccepted"; + break; + + case TransferSyntax_JpegLossless: + configuration = "JpegLosslessTransferSyntaxAccepted"; + break; + + case TransferSyntax_Jpip: + configuration = "JpipTransferSyntaxAccepted"; + break; + + case TransferSyntax_Mpeg2: + configuration = "Mpeg2TransferSyntaxAccepted"; + break; + + case TransferSyntax_Rle: + configuration = "RleTransferSyntaxAccepted"; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + { + std::string lua = "Is" + configuration; + + ServerContext::LuaContextLocker locker(context_); + + if (locker.GetLua().IsExistingFunction(lua.c_str())) + { + LuaFunctionCall call(locker.GetLua(), lua.c_str()); + call.PushString(callingAet); + call.PushString(callingIp); + return call.ExecutePredicate(); + } + } + + return Configuration::GetGlobalBoolParameter(configuration, true); + } }; @@ -186,10 +264,12 @@ { static const char* HTTP_FILTER = "IncomingHttpRequestFilter"; + ServerContext::LuaContextLocker locker(context_); + // Test if the instance must be filtered out - if (context_.GetLuaContext().IsExistingFunction(HTTP_FILTER)) + if (locker.GetLua().IsExistingFunction(HTTP_FILTER)) { - LuaFunctionCall call(context_.GetLuaContext(), HTTP_FILTER); + LuaFunctionCall call(locker.GetLua(), HTTP_FILTER); switch (method) { @@ -229,7 +309,7 @@ }; -void PrintHelp(char* path) +static void PrintHelp(char* path) { std::cout << "Usage: " << path << " [OPTION]... [CONFIGURATION]" << std::endl @@ -256,11 +336,11 @@ } -void PrintVersion(char* path) +static void PrintVersion(char* path) { std::cout << path << " " << ORTHANC_VERSION << std::endl - << "Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege (Belgium) " << std::endl + << "Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics Department, University Hospital 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 @@ -269,6 +349,221 @@ } + +static void LoadLuaScripts(ServerContext& context) +{ + std::list<std::string> luaScripts; + Configuration::GetGlobalListOfStringsParameter(luaScripts, "LuaScripts"); + for (std::list<std::string>::const_iterator + it = luaScripts.begin(); it != luaScripts.end(); ++it) + { + std::string path = Configuration::InterpretStringParameterAsPath(*it); + LOG(WARNING) << "Installing the Lua scripts from: " << path; + std::string script; + Toolbox::ReadFile(script, path); + + ServerContext::LuaContextLocker locker(context); + locker.GetLua().Execute(script); + } +} + + +static void LoadPlugins(PluginsManager& pluginsManager) +{ + std::list<std::string> plugins; + Configuration::GetGlobalListOfStringsParameter(plugins, "Plugins"); + for (std::list<std::string>::const_iterator + it = plugins.begin(); it != plugins.end(); ++it) + { + std::string path = Configuration::InterpretStringParameterAsPath(*it); + LOG(WARNING) << "Loading plugin(s) from: " << path; + pluginsManager.RegisterPlugin(path); + } +} + + + +static bool StartOrthanc(int argc, char *argv[]) +{ +#if ENABLE_PLUGINS == 1 + OrthancPlugins orthancPlugins; + orthancPlugins.SetCommandLineArguments(argc, argv); + PluginsManager pluginsManager; + pluginsManager.RegisterServiceProvider(orthancPlugins); + LoadPlugins(pluginsManager); +#endif + + // "storage" and "database" must be declared BEFORE "ServerContext + // context", to avoid mess in the invokation order of the destructors. + std::auto_ptr<IDatabaseWrapper> database; + std::auto_ptr<IStorageArea> storage; + std::auto_ptr<ServerContext> context; + + if (orthancPlugins.HasDatabase()) + { + context.reset(new ServerContext(orthancPlugins.GetDatabase())); + } + else + { + database.reset(Configuration::CreateDatabaseWrapper()); + context.reset(new ServerContext(*database)); + } + + context->SetCompressionEnabled(Configuration::GetGlobalBoolParameter("StorageCompression", false)); + context->SetStoreMD5ForAttachments(Configuration::GetGlobalBoolParameter("StoreMD5ForAttachments", true)); + + LoadLuaScripts(*context); + + try + { + context->GetIndex().SetMaximumPatientCount(Configuration::GetGlobalIntegerParameter("MaximumPatientCount", 0)); + } + catch (...) + { + context->GetIndex().SetMaximumPatientCount(0); + } + + try + { + uint64_t size = Configuration::GetGlobalIntegerParameter("MaximumStorageSize", 0); + context->GetIndex().SetMaximumStorageSize(size * 1024 * 1024); + } + catch (...) + { + context->GetIndex().SetMaximumStorageSize(0); + } + + MyDicomServerFactory serverFactory(*context); + bool isReset = false; + + { + // DICOM server + DicomServer dicomServer; + OrthancApplicationEntityFilter dicomFilter(*context); + dicomServer.SetCalledApplicationEntityTitleCheck(Configuration::GetGlobalBoolParameter("DicomCheckCalledAet", false)); + dicomServer.SetStoreRequestHandlerFactory(serverFactory); + dicomServer.SetMoveRequestHandlerFactory(serverFactory); + dicomServer.SetFindRequestHandlerFactory(serverFactory); + dicomServer.SetPortNumber(Configuration::GetGlobalIntegerParameter("DicomPort", 4242)); + dicomServer.SetApplicationEntityTitle(Configuration::GetGlobalStringParameter("DicomAet", "ORTHANC")); + dicomServer.SetApplicationEntityFilter(dicomFilter); + + // HTTP server + MyIncomingHttpRequestFilter httpFilter(*context); + MongooseServer httpServer; + httpServer.SetPortNumber(Configuration::GetGlobalIntegerParameter("HttpPort", 8042)); + httpServer.SetRemoteAccessAllowed(Configuration::GetGlobalBoolParameter("RemoteAccessAllowed", false)); + httpServer.SetKeepAliveEnabled(Configuration::GetGlobalBoolParameter("KeepAlive", false)); + httpServer.SetIncomingHttpRequestFilter(httpFilter); + + httpServer.SetAuthenticationEnabled(Configuration::GetGlobalBoolParameter("AuthenticationEnabled", false)); + Configuration::SetupRegisteredUsers(httpServer); + + if (Configuration::GetGlobalBoolParameter("SslEnabled", false)) + { + std::string certificate = Configuration::InterpretStringParameterAsPath( + Configuration::GetGlobalStringParameter("SslCertificate", "certificate.pem")); + httpServer.SetSslEnabled(true); + httpServer.SetSslCertificate(certificate.c_str()); + } + else + { + httpServer.SetSslEnabled(false); + } + + OrthancRestApi restApi(*context); + +#if ORTHANC_STANDALONE == 1 + EmbeddedResourceHttpHandler staticResources("/app", EmbeddedResources::ORTHANC_EXPLORER); +#else + FilesystemHttpHandler staticResources("/app", ORTHANC_PATH "/OrthancExplorer"); +#endif + +#if ENABLE_PLUGINS == 1 + orthancPlugins.SetServerContext(*context); + httpServer.RegisterHandler(orthancPlugins); + context->SetOrthancPlugins(pluginsManager, orthancPlugins); +#endif + + httpServer.RegisterHandler(staticResources); + httpServer.RegisterHandler(restApi); + + +#if ENABLE_PLUGINS == 1 + // Prepare the storage area + if (orthancPlugins.HasStorageArea()) + { + LOG(WARNING) << "Using a custom storage area from plugins"; + storage.reset(orthancPlugins.GetStorageArea()); + } + else +#endif + { + storage.reset(Configuration::CreateStorageArea()); + } + + context->SetStorageArea(*storage); + + + // GO !!! Start the requested servers + if (Configuration::GetGlobalBoolParameter("HttpServerEnabled", true)) + { +#if ENABLE_PLUGINS == 1 + orthancPlugins.SetOrthancRestApi(restApi); +#endif + + httpServer.Start(); + LOG(WARNING) << "HTTP server listening on port: " << httpServer.GetPortNumber(); + } + else + { + LOG(WARNING) << "The HTTP server is disabled"; + } + + if (Configuration::GetGlobalBoolParameter("DicomServerEnabled", true)) + { + dicomServer.Start(); + LOG(WARNING) << "DICOM server listening on port: " << dicomServer.GetPortNumber(); + } + else + { + LOG(WARNING) << "The DICOM server is disabled"; + } + + LOG(WARNING) << "Orthanc has started"; + Toolbox::ServerBarrier(restApi.ResetRequestReceivedFlag()); + isReset = restApi.ResetRequestReceivedFlag(); + + if (isReset) + { + LOG(WARNING) << "Reset request received, restarting Orthanc"; + } + + // We're done + LOG(WARNING) << "Orthanc is stopping"; + +#if ENABLE_PLUGINS == 1 + context->ResetOrthancPlugins(); + orthancPlugins.Stop(); + orthancPlugins.ResetOrthancRestApi(); + LOG(WARNING) << " Plugins have stopped"; +#endif + + dicomServer.Stop(); + LOG(WARNING) << " DICOM server has stopped"; + + httpServer.Stop(); + LOG(WARNING) << " HTTP server has stopped"; + } + + serverFactory.Done(); + + return isReset; +} + + + + int main(int argc, char* argv[]) { // Initialize Google's logging library. @@ -327,178 +622,78 @@ google::InitGoogleLogging("Orthanc"); + const char* configurationFile = NULL; + for (int i = 1; i < argc; i++) + { + // Use the first argument that does not start with a "-" as + // the configuration file + if (argv[i][0] != '-') + { + configurationFile = argv[i]; + } + } + + LOG(WARNING) << "Orthanc version: " << ORTHANC_VERSION; + int status = 0; try { - bool isInitialized = false; - if (argc >= 2) + if (1) { - for (int i = 1; i < argc; i++) - { - // Use the first argument that does not start with a "-" as - // the configuration file - if (argv[i][0] != '-') - { - OrthancInitialize(argv[i]); - isInitialized = true; - } - } - } - - if (!isInitialized) - { - OrthancInitialize(); - } + DicomUserConnection c; + c.SetLocalApplicationEntityTitle("ORTHANC"); + c.SetRemoteApplicationEntityTitle("ORTHANC"); + c.SetRemoteHost("localhost"); + c.SetRemotePort(4343); + c.Open(); - std::string storageDirectoryStr = Configuration::GetGlobalStringParameter("StorageDirectory", "OrthancStorage"); - boost::filesystem::path storageDirectory = Configuration::InterpretStringParameterAsPath(storageDirectoryStr); - boost::filesystem::path indexDirectory = Configuration::InterpretStringParameterAsPath( - Configuration::GetGlobalStringParameter("IndexDirectory", storageDirectoryStr)); - ServerContext context(storageDirectory, indexDirectory); - - LOG(WARNING) << "Storage directory: " << storageDirectory; - LOG(WARNING) << "Index directory: " << indexDirectory; + DicomMap m; // Delphine + m.SetValue(DICOM_TAG_PATIENT_ID, "5423962"); + m.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "1.2.840.113845.11.1000000001951524609.20121203131451.1457891"); + m.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, "1.2.840.113619.2.278.3.262930758.589.1354512768.115"); + m.SetValue(DICOM_TAG_SOP_INSTANCE_UID, "1.3.12.2.1107.5.2.33.37097.2012041613043195815872177"); - context.SetCompressionEnabled(Configuration::GetGlobalBoolParameter("StorageCompression", false)); - context.SetStoreMD5ForAttachments(Configuration::GetGlobalBoolParameter("StoreMD5ForAttachments", true)); + DicomFindAnswers fnd; + c.FindInstance(fnd, m); + //c.FindSeries(fnd, m); - std::list<std::string> luaScripts; - Configuration::GetGlobalListOfStringsParameter(luaScripts, "LuaScripts"); - for (std::list<std::string>::const_iterator - it = luaScripts.begin(); it != luaScripts.end(); ++it) - { - std::string path = Configuration::InterpretStringParameterAsPath(*it); - LOG(WARNING) << "Installing the Lua scripts from: " << path; - std::string script; - Toolbox::ReadFile(script, path); - context.GetLuaContext().Execute(script); + printf("ok %d\n", fnd.GetSize()); } - try - { - context.GetIndex().SetMaximumPatientCount(Configuration::GetGlobalIntegerParameter("MaximumPatientCount", 0)); - } - catch (...) - { - context.GetIndex().SetMaximumPatientCount(0); - } - - try - { - uint64_t size = Configuration::GetGlobalIntegerParameter("MaximumStorageSize", 0); - context.GetIndex().SetMaximumStorageSize(size * 1024 * 1024); - } - catch (...) - { - context.GetIndex().SetMaximumStorageSize(0); - } - - MyDicomServerFactory serverFactory(context); - + for (;;) { - // DICOM server - DicomServer dicomServer; - OrthancApplicationEntityFilter dicomFilter; - dicomServer.SetCalledApplicationEntityTitleCheck(Configuration::GetGlobalBoolParameter("DicomCheckCalledAet", false)); - dicomServer.SetStoreRequestHandlerFactory(serverFactory); - dicomServer.SetMoveRequestHandlerFactory(serverFactory); - dicomServer.SetFindRequestHandlerFactory(serverFactory); - dicomServer.SetPortNumber(Configuration::GetGlobalIntegerParameter("DicomPort", 4242)); - dicomServer.SetApplicationEntityTitle(Configuration::GetGlobalStringParameter("DicomAet", "ORTHANC")); - dicomServer.SetApplicationEntityFilter(dicomFilter); + OrthancInitialize(configurationFile); - // HTTP server - MyIncomingHttpRequestFilter httpFilter(context); - MongooseServer httpServer; - httpServer.SetPortNumber(Configuration::GetGlobalIntegerParameter("HttpPort", 8042)); - httpServer.SetRemoteAccessAllowed(Configuration::GetGlobalBoolParameter("RemoteAccessAllowed", false)); - httpServer.SetIncomingHttpRequestFilter(httpFilter); - - httpServer.SetAuthenticationEnabled(Configuration::GetGlobalBoolParameter("AuthenticationEnabled", false)); - Configuration::SetupRegisteredUsers(httpServer); - - if (Configuration::GetGlobalBoolParameter("SslEnabled", false)) + bool reset = StartOrthanc(argc, argv); + if (reset) { - std::string certificate = Configuration::InterpretStringParameterAsPath( - Configuration::GetGlobalStringParameter("SslCertificate", "certificate.pem")); - httpServer.SetSslEnabled(true); - httpServer.SetSslCertificate(certificate.c_str()); + OrthancFinalize(); } else { - httpServer.SetSslEnabled(false); - } - -#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(context)); - - // GO !!! Start the requested servers - if (Configuration::GetGlobalBoolParameter("HttpServerEnabled", true)) - { - httpServer.Start(); - LOG(WARNING) << "HTTP server listening on port: " << httpServer.GetPortNumber(); - } - else - { - LOG(WARNING) << "The HTTP server is disabled"; - } - - if (Configuration::GetGlobalBoolParameter("DicomServerEnabled", true)) - { - dicomServer.Start(); - LOG(WARNING) << "DICOM server listening on port: " << dicomServer.GetPortNumber(); - } - else - { - LOG(WARNING) << "The DICOM server is disabled"; + break; } - - - { - DicomUserConnection c; - c.SetLocalApplicationEntityTitle("ORTHANC"); - c.SetDistantApplicationEntityTitle("ORTHANC"); - c.SetDistantHost("localhost"); - c.SetDistantPort(4343); - c.Open(); - - DicomMap m; // Delphine - m.SetValue(DICOM_TAG_PATIENT_ID, "5423962"); - m.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "1.2.840.113845.11.1000000001951524609.20121203131451.1457891"); - m.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, "1.2.840.113619.2.278.3.262930758.589.1354512768.115"); - m.SetValue(DICOM_TAG_SOP_INSTANCE_UID, "1.3.12.2.1107.5.2.33.37097.2012041613043195815872177"); - - DicomFindAnswers fnd; - c.FindInstance(fnd, m); - //c.FindSeries(fnd, m); - - printf("ok %d\n", fnd.GetSize()); - } - - - LOG(WARNING) << "Orthanc has started"; - Toolbox::ServerBarrier(); - - // We're done - LOG(WARNING) << "Orthanc is stopping"; } - - serverFactory.Done(); + } + catch (const OrthancException& e) + { + LOG(ERROR) << "Uncaught exception, stopping now: [" << e.What() << "]"; + status = -1; } - catch (OrthancException& e) + catch (const std::exception& e) { - LOG(ERROR) << "EXCEPTION [" << e.What() << "]"; + LOG(ERROR) << "Uncaught exception, stopping now: [" << e.what() << "]"; + status = -1; + } + catch (const std::string& s) + { + LOG(ERROR) << "Uncaught exception, stopping now: [" << s << "]"; status = -1; } catch (...) { - LOG(ERROR) << "NATIVE EXCEPTION"; + LOG(ERROR) << "Native exception, stopping now. Check your plugins, if any."; status = -1; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Engine/IPluginServiceProvider.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,51 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "../Include/OrthancCPlugin.h" + +#include <boost/noncopyable.hpp> + +namespace Orthanc +{ + class IPluginServiceProvider : boost::noncopyable + { + public: + virtual ~IPluginServiceProvider() + { + } + + virtual bool InvokeService(_OrthancPluginService service, + const void* parameters) = 0; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Engine/OrthancPluginDatabase.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,1113 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "OrthancPluginDatabase.h" + +#include "../../Core/OrthancException.h" + +#include <cassert> +#include <glog/logging.h> + +namespace Orthanc +{ + static OrthancPluginResourceType Convert(ResourceType type) + { + switch (type) + { + case ResourceType_Patient: + return OrthancPluginResourceType_Patient; + + case ResourceType_Study: + return OrthancPluginResourceType_Study; + + case ResourceType_Series: + return OrthancPluginResourceType_Series; + + case ResourceType_Instance: + return OrthancPluginResourceType_Instance; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + static ResourceType Convert(OrthancPluginResourceType type) + { + switch (type) + { + case OrthancPluginResourceType_Patient: + return ResourceType_Patient; + + case OrthancPluginResourceType_Study: + return ResourceType_Study; + + case OrthancPluginResourceType_Series: + return ResourceType_Series; + + case OrthancPluginResourceType_Instance: + return ResourceType_Instance; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + static FileInfo Convert(const OrthancPluginAttachment& attachment) + { + return FileInfo(attachment.uuid, + static_cast<FileContentType>(attachment.contentType), + attachment.uncompressedSize, + attachment.uncompressedHash, + static_cast<CompressionType>(attachment.compressionType), + attachment.compressedSize, + attachment.compressedHash); + } + + + void OrthancPluginDatabase::ResetAnswers() + { + type_ = _OrthancPluginDatabaseAnswerType_None; + + answerDicomMap_ = NULL; + answerChanges_ = NULL; + answerExportedResources_ = NULL; + answerDone_ = NULL; + } + + + void OrthancPluginDatabase::ForwardAnswers(std::list<int64_t>& target) + { + if (type_ != _OrthancPluginDatabaseAnswerType_None && + type_ != _OrthancPluginDatabaseAnswerType_Int64) + { + throw OrthancException(ErrorCode_Plugin); + } + + target.clear(); + + if (type_ == _OrthancPluginDatabaseAnswerType_Int64) + { + for (std::list<int64_t>::const_iterator + it = answerInt64_.begin(); it != answerInt64_.end(); ++it) + { + target.push_back(*it); + } + } + } + + + void OrthancPluginDatabase::ForwardAnswers(std::list<std::string>& target) + { + if (type_ != _OrthancPluginDatabaseAnswerType_None && + type_ != _OrthancPluginDatabaseAnswerType_String) + { + throw OrthancException(ErrorCode_Plugin); + } + + target.clear(); + + if (type_ == _OrthancPluginDatabaseAnswerType_String) + { + for (std::list<std::string>::const_iterator + it = answerStrings_.begin(); it != answerStrings_.end(); ++it) + { + target.push_back(*it); + } + } + } + + + bool OrthancPluginDatabase::ForwardSingleAnswer(std::string& target) + { + if (type_ == _OrthancPluginDatabaseAnswerType_None) + { + return false; + } + else if (type_ == _OrthancPluginDatabaseAnswerType_String && + answerStrings_.size() == 1) + { + target = answerStrings_.front(); + return true; + } + else + { + throw OrthancException(ErrorCode_Plugin); + } + } + + + bool OrthancPluginDatabase::ForwardSingleAnswer(int64_t& target) + { + if (type_ == _OrthancPluginDatabaseAnswerType_None) + { + return false; + } + else if (type_ == _OrthancPluginDatabaseAnswerType_Int64 && + answerInt64_.size() == 1) + { + target = answerInt64_.front(); + return true; + } + else + { + throw OrthancException(ErrorCode_Plugin); + } + } + + + OrthancPluginDatabase::OrthancPluginDatabase(const OrthancPluginDatabaseBackend& backend, + void *payload) : + type_(_OrthancPluginDatabaseAnswerType_None), + backend_(backend), + payload_(payload), + listener_(NULL), + answerDicomMap_(NULL), + answerChanges_(NULL), + answerExportedResources_(NULL), + answerDone_(NULL) + { + } + + + void OrthancPluginDatabase::AddAttachment(int64_t id, + const FileInfo& attachment) + { + OrthancPluginAttachment tmp; + tmp.uuid = attachment.GetUuid().c_str(); + tmp.contentType = static_cast<int32_t>(attachment.GetContentType()); + tmp.uncompressedSize = attachment.GetUncompressedSize(); + tmp.uncompressedHash = attachment.GetUncompressedMD5().c_str(); + tmp.compressionType = static_cast<int32_t>(attachment.GetCompressionType()); + tmp.compressedSize = attachment.GetCompressedSize(); + tmp.compressedHash = attachment.GetCompressedMD5().c_str(); + + if (backend_.addAttachment(payload_, id, &tmp) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + } + + + void OrthancPluginDatabase::AttachChild(int64_t parent, + int64_t child) + { + if (backend_.attachChild(payload_, parent, child) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + } + + + void OrthancPluginDatabase::ClearChanges() + { + if (backend_.clearChanges(payload_) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + } + + + void OrthancPluginDatabase::ClearExportedResources() + { + if (backend_.clearExportedResources(payload_) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + } + + + int64_t OrthancPluginDatabase::CreateResource(const std::string& publicId, + ResourceType type) + { + int64_t id; + + if (backend_.createResource(&id, payload_, publicId.c_str(), Convert(type)) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + + return id; + } + + + void OrthancPluginDatabase::DeleteAttachment(int64_t id, + FileContentType attachment) + { + if (backend_.deleteAttachment(payload_, id, static_cast<int32_t>(attachment)) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + } + + + void OrthancPluginDatabase::DeleteMetadata(int64_t id, + MetadataType type) + { + if (backend_.deleteMetadata(payload_, id, static_cast<int32_t>(type)) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + } + + + void OrthancPluginDatabase::DeleteResource(int64_t id) + { + if (backend_.deleteResource(payload_, id) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + } + + + void OrthancPluginDatabase::GetAllMetadata(std::map<MetadataType, std::string>& target, + int64_t id) + { + std::list<MetadataType> metadata; + ListAvailableMetadata(metadata, id); + + target.clear(); + + for (std::list<MetadataType>::const_iterator + it = metadata.begin(); it != metadata.end(); ++it) + { + std::string value; + if (!LookupMetadata(value, id, *it)) + { + throw OrthancException(ErrorCode_Plugin); + } + + target[*it] = value; + } + } + + + void OrthancPluginDatabase::GetAllPublicIds(std::list<std::string>& target, + ResourceType resourceType) + { + ResetAnswers(); + + if (backend_.getAllPublicIds(GetContext(), payload_, Convert(resourceType)) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + + ForwardAnswers(target); + } + + + + void OrthancPluginDatabase::GetChanges(std::list<ServerIndexChange>& target /*out*/, + bool& done /*out*/, + int64_t since, + uint32_t maxResults) + { + ResetAnswers(); + answerChanges_ = ⌖ + answerDone_ = &done; + done = false; + + if (backend_.getChanges(GetContext(), payload_, since, maxResults) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + } + + + void OrthancPluginDatabase::GetChildrenInternalId(std::list<int64_t>& target, + int64_t id) + { + ResetAnswers(); + + if (backend_.getChildrenInternalId(GetContext(), payload_, id) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + + ForwardAnswers(target); + } + + + void OrthancPluginDatabase::GetChildrenPublicId(std::list<std::string>& target, + int64_t id) + { + ResetAnswers(); + + if (backend_.getChildrenPublicId(GetContext(), payload_, id) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + + ForwardAnswers(target); + } + + + void OrthancPluginDatabase::GetExportedResources(std::list<ExportedResource>& target /*out*/, + bool& done /*out*/, + int64_t since, + uint32_t maxResults) + { + ResetAnswers(); + answerExportedResources_ = ⌖ + answerDone_ = &done; + done = false; + + if (backend_.getExportedResources(GetContext(), payload_, since, maxResults) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + } + + + void OrthancPluginDatabase::GetLastChange(std::list<ServerIndexChange>& target /*out*/) + { + bool ignored = false; + + ResetAnswers(); + answerChanges_ = ⌖ + answerDone_ = &ignored; + + if (backend_.getLastChange(GetContext(), payload_) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + } + + + void OrthancPluginDatabase::GetLastExportedResource(std::list<ExportedResource>& target /*out*/) + { + bool ignored = false; + + ResetAnswers(); + answerExportedResources_ = ⌖ + answerDone_ = &ignored; + + if (backend_.getLastExportedResource(GetContext(), payload_) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + } + + + void OrthancPluginDatabase::GetMainDicomTags(DicomMap& map, + int64_t id) + { + ResetAnswers(); + answerDicomMap_ = ↦ + + if (backend_.getMainDicomTags(GetContext(), payload_, id) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + } + + + std::string OrthancPluginDatabase::GetPublicId(int64_t resourceId) + { + ResetAnswers(); + std::string s; + + if (backend_.getPublicId(GetContext(), payload_, resourceId) != 0 || + !ForwardSingleAnswer(s)) + { + throw OrthancException(ErrorCode_Plugin); + } + + return s; + } + + + uint64_t OrthancPluginDatabase::GetResourceCount(ResourceType resourceType) + { + uint64_t count; + + if (backend_.getResourceCount(&count, payload_, Convert(resourceType)) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + + return count; + } + + + ResourceType OrthancPluginDatabase::GetResourceType(int64_t resourceId) + { + OrthancPluginResourceType type; + + if (backend_.getResourceType(&type, payload_, resourceId) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + + return Convert(type); + } + + + uint64_t OrthancPluginDatabase::GetTotalCompressedSize() + { + uint64_t size; + + if (backend_.getTotalCompressedSize(&size, payload_) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + + return size; + } + + + uint64_t OrthancPluginDatabase::GetTotalUncompressedSize() + { + uint64_t size; + + if (backend_.getTotalUncompressedSize(&size, payload_) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + + return size; + } + + + bool OrthancPluginDatabase::IsExistingResource(int64_t internalId) + { + int32_t existing; + + if (backend_.isExistingResource(&existing, payload_, internalId) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + + return existing; + } + + + bool OrthancPluginDatabase::IsProtectedPatient(int64_t internalId) + { + int32_t isProtected; + + if (backend_.isProtectedPatient(&isProtected, payload_, internalId) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + + return isProtected; + } + + + void OrthancPluginDatabase::ListAvailableMetadata(std::list<MetadataType>& target, + int64_t id) + { + ResetAnswers(); + + if (backend_.listAvailableMetadata(GetContext(), payload_, id) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + + if (type_ != _OrthancPluginDatabaseAnswerType_None && + type_ != _OrthancPluginDatabaseAnswerType_Int32) + { + throw OrthancException(ErrorCode_Plugin); + } + + target.clear(); + + if (type_ == _OrthancPluginDatabaseAnswerType_Int32) + { + for (std::list<int32_t>::const_iterator + it = answerInt32_.begin(); it != answerInt32_.end(); ++it) + { + target.push_back(static_cast<MetadataType>(*it)); + } + } + } + + + void OrthancPluginDatabase::ListAvailableAttachments(std::list<FileContentType>& target, + int64_t id) + { + ResetAnswers(); + + if (backend_.listAvailableAttachments(GetContext(), payload_, id) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + + if (type_ != _OrthancPluginDatabaseAnswerType_None && + type_ != _OrthancPluginDatabaseAnswerType_Int32) + { + throw OrthancException(ErrorCode_Plugin); + } + + target.clear(); + + if (type_ == _OrthancPluginDatabaseAnswerType_Int32) + { + for (std::list<int32_t>::const_iterator + it = answerInt32_.begin(); it != answerInt32_.end(); ++it) + { + target.push_back(static_cast<FileContentType>(*it)); + } + } + } + + + void OrthancPluginDatabase::LogChange(int64_t internalId, + const ServerIndexChange& change) + { + OrthancPluginChange tmp; + tmp.seq = change.GetSeq(); + tmp.changeType = static_cast<int32_t>(change.GetChangeType()); + tmp.resourceType = Convert(change.GetResourceType()); + tmp.publicId = change.GetPublicId().c_str(); + tmp.date = change.GetDate().c_str(); + + if (backend_.logChange(payload_, &tmp) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + } + + + void OrthancPluginDatabase::LogExportedResource(const ExportedResource& resource) + { + OrthancPluginExportedResource tmp; + tmp.seq = resource.GetSeq(); + tmp.resourceType = Convert(resource.GetResourceType()); + tmp.publicId = resource.GetPublicId().c_str(); + tmp.modality = resource.GetModality().c_str(); + tmp.date = resource.GetDate().c_str(); + tmp.patientId = resource.GetPatientId().c_str(); + tmp.studyInstanceUid = resource.GetStudyInstanceUid().c_str(); + tmp.seriesInstanceUid = resource.GetSeriesInstanceUid().c_str(); + tmp.sopInstanceUid = resource.GetSopInstanceUid().c_str(); + + if (backend_.logExportedResource(payload_, &tmp) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + } + + + bool OrthancPluginDatabase::LookupAttachment(FileInfo& attachment, + int64_t id, + FileContentType contentType) + { + ResetAnswers(); + + if (backend_.lookupAttachment(GetContext(), payload_, id, static_cast<int32_t>(contentType))) + { + throw OrthancException(ErrorCode_Plugin); + } + + if (type_ == _OrthancPluginDatabaseAnswerType_None) + { + return false; + } + else if (type_ == _OrthancPluginDatabaseAnswerType_Attachment && + answerAttachments_.size() == 1) + { + attachment = answerAttachments_.front(); + return true; + } + else + { + throw OrthancException(ErrorCode_Plugin); + } + } + + + bool OrthancPluginDatabase::LookupGlobalProperty(std::string& target, + GlobalProperty property) + { + ResetAnswers(); + + if (backend_.lookupGlobalProperty(GetContext(), payload_, + static_cast<int32_t>(property))) + { + throw OrthancException(ErrorCode_Plugin); + } + + return ForwardSingleAnswer(target); + } + + + void OrthancPluginDatabase::LookupIdentifier(std::list<int64_t>& target, + const DicomTag& tag, + const std::string& value) + { + ResetAnswers(); + + OrthancPluginDicomTag tmp; + tmp.group = tag.GetGroup(); + tmp.element = tag.GetElement(); + tmp.value = value.c_str(); + + if (backend_.lookupIdentifier(GetContext(), payload_, &tmp) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + + ForwardAnswers(target); + } + + + void OrthancPluginDatabase::LookupIdentifier(std::list<int64_t>& target, + const std::string& value) + { + ResetAnswers(); + + if (backend_.lookupIdentifier2(GetContext(), payload_, value.c_str()) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + + ForwardAnswers(target); + } + + + bool OrthancPluginDatabase::LookupMetadata(std::string& target, + int64_t id, + MetadataType type) + { + ResetAnswers(); + + if (backend_.lookupMetadata(GetContext(), payload_, id, static_cast<int32_t>(type))) + { + throw OrthancException(ErrorCode_Plugin); + } + + return ForwardSingleAnswer(target); + } + + + bool OrthancPluginDatabase::LookupParent(int64_t& parentId, + int64_t resourceId) + { + ResetAnswers(); + + if (backend_.lookupParent(GetContext(), payload_, resourceId)) + { + throw OrthancException(ErrorCode_Plugin); + } + + return ForwardSingleAnswer(parentId); + } + + + bool OrthancPluginDatabase::LookupResource(int64_t& id, + ResourceType& type, + const std::string& publicId) + { + ResetAnswers(); + + if (backend_.lookupResource(GetContext(), payload_, publicId.c_str())) + { + throw OrthancException(ErrorCode_Plugin); + } + + if (type_ == _OrthancPluginDatabaseAnswerType_None) + { + return false; + } + else if (type_ == _OrthancPluginDatabaseAnswerType_Resource && + answerResources_.size() == 1) + { + id = answerResources_.front().first; + type = answerResources_.front().second; + return true; + } + else + { + throw OrthancException(ErrorCode_Plugin); + } + } + + + bool OrthancPluginDatabase::SelectPatientToRecycle(int64_t& internalId) + { + ResetAnswers(); + + if (backend_.selectPatientToRecycle(GetContext(), payload_)) + { + throw OrthancException(ErrorCode_Plugin); + } + + return ForwardSingleAnswer(internalId); + } + + + bool OrthancPluginDatabase::SelectPatientToRecycle(int64_t& internalId, + int64_t patientIdToAvoid) + { + ResetAnswers(); + + if (backend_.selectPatientToRecycle2(GetContext(), payload_, patientIdToAvoid)) + { + throw OrthancException(ErrorCode_Plugin); + } + + return ForwardSingleAnswer(internalId); + } + + + void OrthancPluginDatabase::SetGlobalProperty(GlobalProperty property, + const std::string& value) + { + if (backend_.setGlobalProperty(payload_, static_cast<int32_t>(property), + value.c_str()) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + } + + + void OrthancPluginDatabase::SetMainDicomTag(int64_t id, + const DicomTag& tag, + const std::string& value) + { + int32_t status; + OrthancPluginDicomTag tmp; + tmp.group = tag.GetGroup(); + tmp.element = tag.GetElement(); + tmp.value = value.c_str(); + + if (tag.IsIdentifier()) + { + status = backend_.setIdentifierTag(payload_, id, &tmp); + } + else + { + status = backend_.setMainDicomTag(payload_, id, &tmp); + } + + if (status != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + } + + + void OrthancPluginDatabase::SetMetadata(int64_t id, + MetadataType type, + const std::string& value) + { + if (backend_.setMetadata(payload_, id, static_cast<int32_t>(type), + value.c_str()) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + } + + + void OrthancPluginDatabase::SetProtectedPatient(int64_t internalId, + bool isProtected) + { + if (backend_.setProtectedPatient(payload_, internalId, isProtected) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + } + + + class OrthancPluginDatabase::Transaction : public SQLite::ITransaction + { + private: + const OrthancPluginDatabaseBackend& backend_; + void* payload_; + + public: + Transaction(const OrthancPluginDatabaseBackend& backend, + void* payload) : + backend_(backend), + payload_(payload) + { + } + + virtual void Begin() + { + if (backend_.startTransaction(payload_) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + } + + virtual void Rollback() + { + if (backend_.rollbackTransaction(payload_) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + } + + virtual void Commit() + { + if (backend_.commitTransaction(payload_) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + } + }; + + + SQLite::ITransaction* OrthancPluginDatabase::StartTransaction() + { + return new Transaction(backend_, payload_); + } + + + static void ProcessEvent(IServerIndexListener& listener, + const _OrthancPluginDatabaseAnswer& answer) + { + switch (answer.type) + { + case _OrthancPluginDatabaseAnswerType_DeletedAttachment: + { + const OrthancPluginAttachment& attachment = + *reinterpret_cast<const OrthancPluginAttachment*>(answer.valueGeneric); + listener.SignalFileDeleted(Convert(attachment)); + break; + } + + case _OrthancPluginDatabaseAnswerType_RemainingAncestor: + { + ResourceType type = Convert(static_cast<OrthancPluginResourceType>(answer.valueInt32)); + listener.SignalRemainingAncestor(type, answer.valueString); + break; + } + + case _OrthancPluginDatabaseAnswerType_DeletedResource: + { + ResourceType type = Convert(static_cast<OrthancPluginResourceType>(answer.valueInt32)); + ServerIndexChange change(ChangeType_Deleted, type, answer.valueString); + listener.SignalChange(change); + break; + } + + default: + throw OrthancException(ErrorCode_Plugin); + } + } + + + void OrthancPluginDatabase::AnswerReceived(const _OrthancPluginDatabaseAnswer& answer) + { + if (answer.type == _OrthancPluginDatabaseAnswerType_None) + { + throw OrthancException(ErrorCode_Plugin); + } + + if (answer.type == _OrthancPluginDatabaseAnswerType_DeletedAttachment || + answer.type == _OrthancPluginDatabaseAnswerType_DeletedResource || + answer.type == _OrthancPluginDatabaseAnswerType_RemainingAncestor) + { + assert(listener_ != NULL); + ProcessEvent(*listener_, answer); + return; + } + + if (type_ == _OrthancPluginDatabaseAnswerType_None) + { + type_ = answer.type; + + switch (type_) + { + case _OrthancPluginDatabaseAnswerType_Int32: + answerInt32_.clear(); + break; + + case _OrthancPluginDatabaseAnswerType_Int64: + answerInt64_.clear(); + break; + + case _OrthancPluginDatabaseAnswerType_Resource: + answerResources_.clear(); + break; + + case _OrthancPluginDatabaseAnswerType_Attachment: + answerAttachments_.clear(); + break; + + case _OrthancPluginDatabaseAnswerType_String: + answerStrings_.clear(); + break; + + case _OrthancPluginDatabaseAnswerType_DicomTag: + assert(answerDicomMap_ != NULL); + answerDicomMap_->Clear(); + break; + + case _OrthancPluginDatabaseAnswerType_Change: + assert(answerChanges_ != NULL); + answerChanges_->clear(); + break; + + case _OrthancPluginDatabaseAnswerType_ExportedResource: + assert(answerExportedResources_ != NULL); + answerExportedResources_->clear(); + break; + + default: + LOG(ERROR) << "Unhandled type of answer for custom index plugin: " << answer.type; + throw OrthancException(ErrorCode_Plugin); + } + } + else if (type_ != answer.type) + { + LOG(ERROR) << "Error in the plugin protocol: Cannot change the answer type"; + throw OrthancException(ErrorCode_Plugin); + } + + switch (answer.type) + { + case _OrthancPluginDatabaseAnswerType_Int32: + { + answerInt32_.push_back(answer.valueInt32); + break; + } + + case _OrthancPluginDatabaseAnswerType_Int64: + { + answerInt64_.push_back(answer.valueInt64); + break; + } + + case _OrthancPluginDatabaseAnswerType_Resource: + { + OrthancPluginResourceType type = static_cast<OrthancPluginResourceType>(answer.valueInt32); + answerResources_.push_back(std::make_pair(answer.valueInt64, Convert(type))); + break; + } + + case _OrthancPluginDatabaseAnswerType_Attachment: + { + const OrthancPluginAttachment& attachment = + *reinterpret_cast<const OrthancPluginAttachment*>(answer.valueGeneric); + + answerAttachments_.push_back(Convert(attachment)); + break; + } + + case _OrthancPluginDatabaseAnswerType_DicomTag: + { + const OrthancPluginDicomTag& tag = *reinterpret_cast<const OrthancPluginDicomTag*>(answer.valueGeneric); + assert(answerDicomMap_ != NULL); + answerDicomMap_->SetValue(tag.group, tag.element, std::string(tag.value)); + break; + } + + case _OrthancPluginDatabaseAnswerType_String: + { + if (answer.valueString == NULL) + { + throw OrthancException(ErrorCode_Plugin); + } + + if (type_ == _OrthancPluginDatabaseAnswerType_None) + { + type_ = _OrthancPluginDatabaseAnswerType_String; + answerStrings_.clear(); + } + else if (type_ != _OrthancPluginDatabaseAnswerType_String) + { + throw OrthancException(ErrorCode_Plugin); + } + + answerStrings_.push_back(std::string(answer.valueString)); + break; + } + + case _OrthancPluginDatabaseAnswerType_Change: + { + assert(answerDone_ != NULL); + if (answer.valueUint32 == 1) + { + *answerDone_ = true; + } + else if (*answerDone_) + { + throw OrthancException(ErrorCode_Plugin); + } + else + { + const OrthancPluginChange& change = *reinterpret_cast<const OrthancPluginChange*>(answer.valueGeneric); + assert(answerChanges_ != NULL); + answerChanges_->push_back + (ServerIndexChange(change.seq, + static_cast<ChangeType>(change.changeType), + Convert(change.resourceType), + change.publicId, + change.date)); + } + + break; + } + + case _OrthancPluginDatabaseAnswerType_ExportedResource: + { + assert(answerDone_ != NULL); + if (answer.valueUint32 == 1) + { + *answerDone_ = true; + } + else if (*answerDone_) + { + throw OrthancException(ErrorCode_Plugin); + } + else + { + const OrthancPluginExportedResource& exported = + *reinterpret_cast<const OrthancPluginExportedResource*>(answer.valueGeneric); + assert(answerExportedResources_ != NULL); + answerExportedResources_->push_back + (ExportedResource(exported.seq, + Convert(exported.resourceType), + exported.publicId, + exported.modality, + exported.date, + exported.patientId, + exported.studyInstanceUid, + exported.seriesInstanceUid, + exported.sopInstanceUid)); + } + + break; + } + + default: + LOG(ERROR) << "Unhandled type of answer for custom index plugin: " << answer.type; + throw OrthancException(ErrorCode_Plugin); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Engine/OrthancPluginDatabase.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,220 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "../../OrthancServer/IDatabaseWrapper.h" +#include "../Include/OrthancCDatabasePlugin.h" + +namespace Orthanc +{ + class OrthancPluginDatabase : public IDatabaseWrapper + { + private: + class Transaction; + + typedef std::pair<int64_t, ResourceType> AnswerResource; + + _OrthancPluginDatabaseAnswerType type_; + OrthancPluginDatabaseBackend backend_; + void* payload_; + IServerIndexListener* listener_; + + std::list<std::string> answerStrings_; + std::list<int32_t> answerInt32_; + std::list<int64_t> answerInt64_; + std::list<AnswerResource> answerResources_; + std::list<FileInfo> answerAttachments_; + + DicomMap* answerDicomMap_; + std::list<ServerIndexChange>* answerChanges_; + std::list<ExportedResource>* answerExportedResources_; + bool* answerDone_; + + OrthancPluginDatabaseContext* GetContext() + { + return reinterpret_cast<OrthancPluginDatabaseContext*>(this); + } + + void ResetAnswers(); + + void ForwardAnswers(std::list<int64_t>& target); + + void ForwardAnswers(std::list<std::string>& target); + + bool ForwardSingleAnswer(std::string& target); + + bool ForwardSingleAnswer(int64_t& target); + + public: + OrthancPluginDatabase(const OrthancPluginDatabaseBackend& backend, + void *payload); + + virtual void AddAttachment(int64_t id, + const FileInfo& attachment); + + virtual void AttachChild(int64_t parent, + int64_t child); + + virtual void ClearChanges(); + + virtual void ClearExportedResources(); + + virtual int64_t CreateResource(const std::string& publicId, + ResourceType type); + + virtual void DeleteAttachment(int64_t id, + FileContentType attachment); + + virtual void DeleteMetadata(int64_t id, + MetadataType type); + + virtual void DeleteResource(int64_t id); + + virtual void FlushToDisk() + { + } + + virtual bool HasFlushToDisk() const + { + return false; + } + + virtual void GetAllMetadata(std::map<MetadataType, std::string>& target, + int64_t id); + + virtual void GetAllPublicIds(std::list<std::string>& target, + ResourceType resourceType); + + + virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/, + bool& done /*out*/, + int64_t since, + uint32_t maxResults); + + virtual void GetChildrenInternalId(std::list<int64_t>& target, + int64_t id); + + virtual void GetChildrenPublicId(std::list<std::string>& target, + int64_t id); + + virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/, + bool& done /*out*/, + int64_t since, + uint32_t maxResults); + + virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/); + + virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/); + + virtual void GetMainDicomTags(DicomMap& map, + int64_t id); + + virtual std::string GetPublicId(int64_t resourceId); + + virtual uint64_t GetResourceCount(ResourceType resourceType); + + virtual ResourceType GetResourceType(int64_t resourceId); + + virtual uint64_t GetTotalCompressedSize(); + + virtual uint64_t GetTotalUncompressedSize(); + + virtual bool IsExistingResource(int64_t internalId); + + virtual bool IsProtectedPatient(int64_t internalId); + + virtual void ListAvailableMetadata(std::list<MetadataType>& target, + int64_t id); + + virtual void ListAvailableAttachments(std::list<FileContentType>& target, + int64_t id); + + virtual void LogChange(int64_t internalId, + const ServerIndexChange& change); + + virtual void LogExportedResource(const ExportedResource& resource); + + virtual bool LookupAttachment(FileInfo& attachment, + int64_t id, + FileContentType contentType); + + virtual bool LookupGlobalProperty(std::string& target, + GlobalProperty property); + + virtual void LookupIdentifier(std::list<int64_t>& target, + const DicomTag& tag, + const std::string& value); + + virtual void LookupIdentifier(std::list<int64_t>& target, + const std::string& value); + + virtual bool LookupMetadata(std::string& target, + int64_t id, + MetadataType type); + + virtual bool LookupParent(int64_t& parentId, + int64_t resourceId); + + virtual bool LookupResource(int64_t& id, + ResourceType& type, + const std::string& publicId); + + virtual bool SelectPatientToRecycle(int64_t& internalId); + + virtual bool SelectPatientToRecycle(int64_t& internalId, + int64_t patientIdToAvoid); + + virtual void SetGlobalProperty(GlobalProperty property, + const std::string& value); + + virtual void SetMainDicomTag(int64_t id, + const DicomTag& tag, + const std::string& value); + + virtual void SetMetadata(int64_t id, + MetadataType type, + const std::string& value); + + virtual void SetProtectedPatient(int64_t internalId, + bool isProtected); + + virtual SQLite::ITransaction* StartTransaction(); + + virtual void SetListener(IServerIndexListener& listener) + { + listener_ = &listener; + } + + void AnswerReceived(const _OrthancPluginDatabaseAnswer& answer); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Engine/OrthancPlugins.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,1378 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "OrthancPlugins.h" + +#include "../../Core/ChunkedBuffer.h" +#include "../../Core/OrthancException.h" +#include "../../Core/Toolbox.h" +#include "../../Core/HttpServer/HttpOutput.h" +#include "../../Core/ImageFormats/PngWriter.h" +#include "../../OrthancServer/ServerToolbox.h" +#include "../../OrthancServer/OrthancInitialization.h" +#include "../../Core/MultiThreading/SharedMessageQueue.h" + +#include <boost/thread.hpp> +#include <boost/regex.hpp> +#include <glog/logging.h> + +namespace Orthanc +{ + static OrthancPluginResourceType Convert(ResourceType type) + { + switch (type) + { + case ResourceType_Patient: + return OrthancPluginResourceType_Patient; + + case ResourceType_Study: + return OrthancPluginResourceType_Study; + + case ResourceType_Series: + return OrthancPluginResourceType_Series; + + case ResourceType_Instance: + return OrthancPluginResourceType_Instance; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + static OrthancPluginChangeType Convert(ChangeType type) + { + switch (type) + { + case ChangeType_CompletedSeries: + return OrthancPluginChangeType_CompletedSeries; + + case ChangeType_Deleted: + return OrthancPluginChangeType_Deleted; + + case ChangeType_NewChildInstance: + return OrthancPluginChangeType_NewChildInstance; + + case ChangeType_NewInstance: + return OrthancPluginChangeType_NewInstance; + + case ChangeType_NewPatient: + return OrthancPluginChangeType_NewPatient; + + case ChangeType_NewSeries: + return OrthancPluginChangeType_NewSeries; + + case ChangeType_NewStudy: + return OrthancPluginChangeType_NewStudy; + + case ChangeType_StablePatient: + return OrthancPluginChangeType_StablePatient; + + case ChangeType_StableSeries: + return OrthancPluginChangeType_StableSeries; + + case ChangeType_StableStudy: + return OrthancPluginChangeType_StableStudy; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + namespace + { + // Anonymous namespace to avoid clashes between compilation modules + class StringHttpOutput : public IHttpOutputStream + { + private: + ChunkedBuffer buffer_; + + public: + void GetOutput(std::string& output) + { + buffer_.Flatten(output); + } + + virtual void OnHttpStatusReceived(HttpStatus status) + { + if (status != HttpStatus_200_Ok) + { + throw OrthancException(ErrorCode_BadRequest); + } + } + + virtual void Send(bool isHeader, const void* buffer, size_t length) + { + if (!isHeader) + { + buffer_.AddChunk(reinterpret_cast<const char*>(buffer), length); + } + } + }; + + + class PendingChange : public IDynamicObject + { + private: + OrthancPluginChangeType changeType_; + OrthancPluginResourceType resourceType_; + std::string publicId_; + + public: + PendingChange(const ServerIndexChange& change) + { + changeType_ = Convert(change.GetChangeType()); + resourceType_ = Convert(change.GetResourceType()); + publicId_ = change.GetPublicId(); + } + + void Submit(std::list<OrthancPluginOnChangeCallback>& callbacks) + { + for (std::list<OrthancPluginOnChangeCallback>::const_iterator + callback = callbacks.begin(); + callback != callbacks.end(); ++callback) + { + (*callback) (changeType_, resourceType_, publicId_.c_str()); + } + } + }; + } + + + + struct OrthancPlugins::PImpl + { + typedef std::pair<std::string, _OrthancPluginProperty> Property; + + typedef std::pair<boost::regex*, OrthancPluginRestCallback> RestCallback; + typedef std::list<RestCallback> RestCallbacks; + typedef std::list<OrthancPluginOnStoredInstanceCallback> OnStoredCallbacks; + typedef std::list<OrthancPluginOnChangeCallback> OnChangeCallbacks; + typedef std::map<Property, std::string> Properties; + + ServerContext* context_; + RestCallbacks restCallbacks_; + OrthancRestApi* restApi_; + OnStoredCallbacks onStoredCallbacks_; + OnChangeCallbacks onChangeCallbacks_; + bool hasStorageArea_; + _OrthancPluginRegisterStorageArea storageArea_; + boost::recursive_mutex callbackMutex_; + SharedMessageQueue pendingChanges_; + boost::thread changeThread_; + bool done_; + Properties properties_; + int argc_; + char** argv_; + std::auto_ptr<OrthancPluginDatabase> database_; + + PImpl() : + context_(NULL), + restApi_(NULL), + hasStorageArea_(false), + done_(false), + argc_(1), + argv_(NULL) + { + memset(&storageArea_, 0, sizeof(storageArea_)); + } + + + static void ChangeThread(PImpl* that) + { + while (!that->done_) + { + std::auto_ptr<IDynamicObject> obj(that->pendingChanges_.Dequeue(500)); + + if (obj.get() != NULL) + { + boost::recursive_mutex::scoped_lock lock(that->callbackMutex_); + PendingChange& change = *dynamic_cast<PendingChange*>(obj.get()); + change.Submit(that->onChangeCallbacks_); + } + } + } + }; + + + + static char* CopyString(const std::string& str) + { + char *result = reinterpret_cast<char*>(malloc(str.size() + 1)); + if (result == NULL) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + + if (str.size() == 0) + { + result[0] = '\0'; + } + else + { + memcpy(result, &str[0], str.size() + 1); + } + + return result; + } + + + OrthancPlugins::OrthancPlugins() + { + pimpl_.reset(new PImpl()); + pimpl_->changeThread_ = boost::thread(PImpl::ChangeThread, pimpl_.get()); + } + + + void OrthancPlugins::SetServerContext(ServerContext& context) + { + pimpl_->context_ = &context; + } + + + + OrthancPlugins::~OrthancPlugins() + { + Stop(); + + for (PImpl::RestCallbacks::iterator it = pimpl_->restCallbacks_.begin(); + it != pimpl_->restCallbacks_.end(); ++it) + { + // Delete the regular expression associated with this callback + delete it->first; + } + } + + + void OrthancPlugins::Stop() + { + if (!pimpl_->done_) + { + pimpl_->done_ = true; + pimpl_->changeThread_.join(); + } + } + + + + static void ArgumentsToPlugin(std::vector<const char*>& keys, + std::vector<const char*>& values, + const HttpHandler::Arguments& arguments) + { + keys.resize(arguments.size()); + values.resize(arguments.size()); + + size_t pos = 0; + for (HttpHandler::Arguments::const_iterator + it = arguments.begin(); it != arguments.end(); ++it) + { + keys[pos] = it->first.c_str(); + values[pos] = it->second.c_str(); + pos++; + } + } + + + static void ArgumentsToPlugin(std::vector<const char*>& keys, + std::vector<const char*>& values, + const HttpHandler::GetArguments& arguments) + { + keys.resize(arguments.size()); + values.resize(arguments.size()); + + for (size_t i = 0; i < arguments.size(); i++) + { + keys[i] = arguments[i].first.c_str(); + values[i] = arguments[i].second.c_str(); + } + } + + + bool OrthancPlugins::Handle(HttpOutput& output, + HttpMethod method, + const UriComponents& uri, + const Arguments& headers, + const GetArguments& getArguments, + const std::string& postData) + { + std::string flatUri = Toolbox::FlattenUri(uri); + OrthancPluginRestCallback callback = NULL; + + std::vector<std::string> groups; + std::vector<const char*> cgroups; + + // Loop over the callbacks registered by the plugins + bool found = false; + for (PImpl::RestCallbacks::const_iterator it = pimpl_->restCallbacks_.begin(); + it != pimpl_->restCallbacks_.end() && !found; ++it) + { + // Check whether the regular expression associated to this + // callback matches the URI + boost::cmatch what; + if (boost::regex_match(flatUri.c_str(), what, *(it->first))) + { + callback = it->second; + + // Extract the value of the free parameters of the regular expression + if (what.size() > 1) + { + groups.resize(what.size() - 1); + cgroups.resize(what.size() - 1); + for (size_t i = 1; i < what.size(); i++) + { + groups[i - 1] = what[i]; + cgroups[i - 1] = groups[i - 1].c_str(); + } + } + + found = true; + } + } + + if (!found) + { + return false; + } + + LOG(INFO) << "Delegating HTTP request to plugin for URI: " << flatUri; + + std::vector<const char*> getKeys, getValues, headersKeys, headersValues; + + OrthancPluginHttpRequest request; + memset(&request, 0, sizeof(OrthancPluginHttpRequest)); + + ArgumentsToPlugin(headersKeys, headersValues, headers); + + switch (method) + { + case HttpMethod_Get: + request.method = OrthancPluginHttpMethod_Get; + ArgumentsToPlugin(getKeys, getValues, getArguments); + break; + + case HttpMethod_Post: + request.method = OrthancPluginHttpMethod_Post; + break; + + case HttpMethod_Delete: + request.method = OrthancPluginHttpMethod_Delete; + break; + + case HttpMethod_Put: + request.method = OrthancPluginHttpMethod_Put; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + + request.groups = (cgroups.size() ? &cgroups[0] : NULL); + request.groupsCount = cgroups.size(); + request.getCount = getArguments.size(); + request.body = (postData.size() ? &postData[0] : NULL); + request.bodySize = postData.size(); + request.headersCount = headers.size(); + + if (getArguments.size() > 0) + { + request.getKeys = &getKeys[0]; + request.getValues = &getValues[0]; + } + + if (headers.size() > 0) + { + request.headersKeys = &headersKeys[0]; + request.headersValues = &headersValues[0]; + } + + assert(callback != NULL); + int32_t error; + + { + boost::recursive_mutex::scoped_lock lock(pimpl_->callbackMutex_); + error = callback(reinterpret_cast<OrthancPluginRestOutput*>(&output), + flatUri.c_str(), + &request); + } + + if (error < 0) + { + LOG(ERROR) << "Plugin callback failed with error code " << error; + return false; + } + else + { + if (error > 0) + { + LOG(WARNING) << "Plugin callback finished with warning code " << error; + } + + return true; + } + } + + + void OrthancPlugins::SignalStoredInstance(DicomInstanceToStore& instance, + const std::string& instanceId) + { + boost::recursive_mutex::scoped_lock lock(pimpl_->callbackMutex_); + + for (PImpl::OnStoredCallbacks::const_iterator + callback = pimpl_->onStoredCallbacks_.begin(); + callback != pimpl_->onStoredCallbacks_.end(); ++callback) + { + (*callback) (reinterpret_cast<OrthancPluginDicomInstance*>(&instance), + instanceId.c_str()); + } + } + + + + void OrthancPlugins::SignalChange(const ServerIndexChange& change) + { + try + { + pimpl_->pendingChanges_.Enqueue(new PendingChange(change)); + } + catch (OrthancException&) + { + // This change type or resource type is not supported by the plugin SDK + return; + } + } + + + + static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target, + const void* data, + size_t size) + { + target.size = size; + + if (size == 0) + { + target.data = NULL; + } + else + { + target.data = malloc(size); + if (target.data != NULL) + { + memcpy(target.data, data, size); + } + else + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + } + } + + + static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target, + const std::string& str) + { + if (str.size() == 0) + { + target.size = 0; + target.data = NULL; + } + else + { + CopyToMemoryBuffer(target, str.c_str(), str.size()); + } + } + + + void OrthancPlugins::RegisterRestCallback(const void* parameters) + { + const _OrthancPluginRestCallback& p = + *reinterpret_cast<const _OrthancPluginRestCallback*>(parameters); + + LOG(INFO) << "Plugin has registered a REST callback on: " << p.pathRegularExpression; + pimpl_->restCallbacks_.push_back(std::make_pair(new boost::regex(p.pathRegularExpression), p.callback)); + } + + + + void OrthancPlugins::RegisterOnStoredInstanceCallback(const void* parameters) + { + const _OrthancPluginOnStoredInstanceCallback& p = + *reinterpret_cast<const _OrthancPluginOnStoredInstanceCallback*>(parameters); + + LOG(INFO) << "Plugin has registered an OnStoredInstance callback"; + pimpl_->onStoredCallbacks_.push_back(p.callback); + } + + + void OrthancPlugins::RegisterOnChangeCallback(const void* parameters) + { + const _OrthancPluginOnChangeCallback& p = + *reinterpret_cast<const _OrthancPluginOnChangeCallback*>(parameters); + + LOG(INFO) << "Plugin has registered an OnChange callback"; + pimpl_->onChangeCallbacks_.push_back(p.callback); + } + + + + void OrthancPlugins::AnswerBuffer(const void* parameters) + { + const _OrthancPluginAnswerBuffer& p = + *reinterpret_cast<const _OrthancPluginAnswerBuffer*>(parameters); + + HttpOutput* translatedOutput = reinterpret_cast<HttpOutput*>(p.output); + translatedOutput->SetContentType(p.mimeType); + translatedOutput->SendBody(p.answer, p.answerSize); + } + + + void OrthancPlugins::Redirect(const void* parameters) + { + const _OrthancPluginOutputPlusArgument& p = + *reinterpret_cast<const _OrthancPluginOutputPlusArgument*>(parameters); + + HttpOutput* translatedOutput = reinterpret_cast<HttpOutput*>(p.output); + translatedOutput->Redirect(p.argument); + } + + + void OrthancPlugins::SendHttpStatusCode(const void* parameters) + { + const _OrthancPluginSendHttpStatusCode& p = + *reinterpret_cast<const _OrthancPluginSendHttpStatusCode*>(parameters); + + HttpOutput* translatedOutput = reinterpret_cast<HttpOutput*>(p.output); + translatedOutput->SendStatus(static_cast<HttpStatus>(p.status)); + } + + + void OrthancPlugins::SendUnauthorized(const void* parameters) + { + const _OrthancPluginOutputPlusArgument& p = + *reinterpret_cast<const _OrthancPluginOutputPlusArgument*>(parameters); + + HttpOutput* translatedOutput = reinterpret_cast<HttpOutput*>(p.output); + translatedOutput->SendUnauthorized(p.argument); + } + + + void OrthancPlugins::SendMethodNotAllowed(const void* parameters) + { + const _OrthancPluginOutputPlusArgument& p = + *reinterpret_cast<const _OrthancPluginOutputPlusArgument*>(parameters); + + HttpOutput* translatedOutput = reinterpret_cast<HttpOutput*>(p.output); + translatedOutput->SendMethodNotAllowed(p.argument); + } + + + void OrthancPlugins::SetCookie(const void* parameters) + { + const _OrthancPluginSetHttpHeader& p = + *reinterpret_cast<const _OrthancPluginSetHttpHeader*>(parameters); + + HttpOutput* translatedOutput = reinterpret_cast<HttpOutput*>(p.output); + translatedOutput->SetCookie(p.key, p.value); + } + + + void OrthancPlugins::SetHttpHeader(const void* parameters) + { + const _OrthancPluginSetHttpHeader& p = + *reinterpret_cast<const _OrthancPluginSetHttpHeader*>(parameters); + + HttpOutput* translatedOutput = reinterpret_cast<HttpOutput*>(p.output); + translatedOutput->AddHeader(p.key, p.value); + } + + + void OrthancPlugins::CompressAndAnswerPngImage(const void* parameters) + { + const _OrthancPluginCompressAndAnswerPngImage& p = + *reinterpret_cast<const _OrthancPluginCompressAndAnswerPngImage*>(parameters); + + HttpOutput* translatedOutput = reinterpret_cast<HttpOutput*>(p.output); + + PixelFormat format; + switch (p.format) + { + case OrthancPluginPixelFormat_Grayscale8: + format = PixelFormat_Grayscale8; + break; + + case OrthancPluginPixelFormat_Grayscale16: + format = PixelFormat_Grayscale16; + break; + + case OrthancPluginPixelFormat_SignedGrayscale16: + format = PixelFormat_SignedGrayscale16; + break; + + case OrthancPluginPixelFormat_RGB24: + format = PixelFormat_RGB24; + break; + + case OrthancPluginPixelFormat_RGBA32: + format = PixelFormat_RGBA32; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + ImageAccessor accessor; + accessor.AssignReadOnly(format, p.width, p.height, p.pitch, p.buffer); + + PngWriter writer; + std::string png; + writer.WriteToMemory(png, accessor); + + translatedOutput->SetContentType("image/png"); + translatedOutput->SendBody(png); + } + + + void OrthancPlugins::GetDicomForInstance(const void* parameters) + { + assert(pimpl_->context_ != NULL); + + const _OrthancPluginGetDicomForInstance& p = + *reinterpret_cast<const _OrthancPluginGetDicomForInstance*>(parameters); + + std::string dicom; + pimpl_->context_->ReadFile(dicom, p.instanceId, FileContentType_Dicom); + CopyToMemoryBuffer(*p.target, dicom); + } + + + void OrthancPlugins::RestApiGet(const void* parameters, + bool afterPlugins) + { + const _OrthancPluginRestApiGet& p = + *reinterpret_cast<const _OrthancPluginRestApiGet*>(parameters); + + HttpHandler::Arguments headers; // No HTTP header + std::string body; // No body for a GET request + + UriComponents uri; + HttpHandler::GetArguments getArguments; + HttpHandler::ParseGetQuery(uri, getArguments, p.uri); + + StringHttpOutput stream; + HttpOutput http(stream, false /* no keep alive */); + + LOG(INFO) << "Plugin making REST GET call on URI " << p.uri + << (afterPlugins ? " (after plugins)" : " (built-in API)"); + + bool ok = false; + std::string result; + + if (afterPlugins) + { + ok = Handle(http, HttpMethod_Get, uri, headers, getArguments, body); + } + + if (!ok) + { + ok = (pimpl_->restApi_ != NULL && + pimpl_->restApi_->Handle(http, HttpMethod_Get, uri, headers, getArguments, body)); + } + + if (ok) + { + stream.GetOutput(result); + CopyToMemoryBuffer(*p.target, result); + } + else + { + throw OrthancException(ErrorCode_BadRequest); + } + } + + + void OrthancPlugins::RestApiPostPut(bool isPost, + const void* parameters, + bool afterPlugins) + { + const _OrthancPluginRestApiPostPut& p = + *reinterpret_cast<const _OrthancPluginRestApiPostPut*>(parameters); + + HttpHandler::Arguments headers; // No HTTP header + HttpHandler::GetArguments getArguments; // No GET argument for POST/PUT + + UriComponents uri; + Toolbox::SplitUriComponents(uri, p.uri); + + // TODO Avoid unecessary memcpy + std::string body(p.body, p.bodySize); + + StringHttpOutput stream; + HttpOutput http(stream, false /* no keep alive */); + + HttpMethod method = (isPost ? HttpMethod_Post : HttpMethod_Put); + LOG(INFO) << "Plugin making REST " << EnumerationToString(method) << " call on URI " << p.uri + << (afterPlugins ? " (after plugins)" : " (built-in API)"); + + bool ok = false; + std::string result; + + if (afterPlugins) + { + ok = Handle(http, method, uri, headers, getArguments, body); + } + + if (!ok) + { + ok = (pimpl_->restApi_ != NULL && + pimpl_->restApi_->Handle(http, method, uri, headers, getArguments, body)); + } + + if (ok) + { + stream.GetOutput(result); + CopyToMemoryBuffer(*p.target, result); + } + else + { + throw OrthancException(ErrorCode_BadRequest); + } + } + + + void OrthancPlugins::RestApiDelete(const void* parameters, + bool afterPlugins) + { + // The "parameters" point to the URI + UriComponents uri; + Toolbox::SplitUriComponents(uri, reinterpret_cast<const char*>(parameters)); + + HttpHandler::Arguments headers; // No HTTP header + HttpHandler::GetArguments getArguments; // No GET argument for POST/PUT + std::string body; // No body for DELETE + + StringHttpOutput stream; + HttpOutput http(stream, false /* no keep alive */); + + LOG(INFO) << "Plugin making REST DELETE call on URI " + << reinterpret_cast<const char*>(parameters) + << (afterPlugins ? " (after plugins)" : " (built-in API)"); + + bool ok = false; + + if (afterPlugins) + { + ok = Handle(http, HttpMethod_Delete, uri, headers, getArguments, body); + } + + if (!ok) + { + ok = (pimpl_->restApi_ != NULL && + pimpl_->restApi_->Handle(http, HttpMethod_Delete, uri, headers, getArguments, body)); + } + + if (!ok) + { + throw OrthancException(ErrorCode_BadRequest); + } + } + + + void OrthancPlugins::LookupResource(_OrthancPluginService service, + const void* parameters) + { + const _OrthancPluginRetrieveDynamicString& p = + *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters); + + /** + * The enumeration below only uses the tags that are indexed in + * the Orthanc database. It reflects the + * "CandidateResources::ApplyFilter()" method of the + * "OrthancFindRequestHandler" class. + **/ + + DicomTag tag(0, 0); + ResourceType level; + switch (service) + { + case _OrthancPluginService_LookupPatient: + tag = DICOM_TAG_PATIENT_ID; + level = ResourceType_Patient; + break; + + case _OrthancPluginService_LookupStudy: + tag = DICOM_TAG_STUDY_INSTANCE_UID; + level = ResourceType_Study; + break; + + case _OrthancPluginService_LookupStudyWithAccessionNumber: + tag = DICOM_TAG_ACCESSION_NUMBER; + level = ResourceType_Study; + break; + + case _OrthancPluginService_LookupSeries: + tag = DICOM_TAG_SERIES_INSTANCE_UID; + level = ResourceType_Series; + break; + + case _OrthancPluginService_LookupInstance: + tag = DICOM_TAG_SOP_INSTANCE_UID; + level = ResourceType_Instance; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + assert(pimpl_->context_ != NULL); + + std::list<std::string> result; + pimpl_->context_->GetIndex().LookupIdentifier(result, tag, p.argument, level); + + if (result.size() == 1) + { + *p.result = CopyString(result.front()); + } + else + { + throw OrthancException(ErrorCode_UnknownResource); + } + } + + + static void AccessInstanceMetadataInternal(bool checkExistence, + const _OrthancPluginAccessDicomInstance& params, + const DicomInstanceToStore& instance) + { + MetadataType metadata; + + try + { + metadata = StringToMetadata(params.key); + } + catch (OrthancException&) + { + // Unknown metadata + if (checkExistence) + { + *params.resultInt64 = -1; + } + else + { + *params.resultString = NULL; + } + + return; + } + + ServerIndex::MetadataMap::const_iterator it = + instance.GetMetadata().find(std::make_pair(ResourceType_Instance, metadata)); + + if (checkExistence) + { + if (it != instance.GetMetadata().end()) + { + *params.resultInt64 = 1; + } + else + { + *params.resultInt64 = 0; + } + } + else + { + if (it != instance.GetMetadata().end()) + { + *params.resultString = it->second.c_str(); + } + else + { + // Error: Missing metadata + *params.resultString = NULL; + } + } + } + + + static void AccessDicomInstance(_OrthancPluginService service, + const void* parameters) + { + const _OrthancPluginAccessDicomInstance& p = + *reinterpret_cast<const _OrthancPluginAccessDicomInstance*>(parameters); + + DicomInstanceToStore& instance = + *reinterpret_cast<DicomInstanceToStore*>(p.instance); + + switch (service) + { + case _OrthancPluginService_GetInstanceRemoteAet: + *p.resultString = instance.GetRemoteAet().c_str(); + return; + + case _OrthancPluginService_GetInstanceSize: + *p.resultInt64 = instance.GetBufferSize(); + return; + + case _OrthancPluginService_GetInstanceData: + *p.resultString = instance.GetBufferData(); + return; + + case _OrthancPluginService_HasInstanceMetadata: + AccessInstanceMetadataInternal(true, p, instance); + return; + + case _OrthancPluginService_GetInstanceMetadata: + AccessInstanceMetadataInternal(false, p, instance); + return; + + case _OrthancPluginService_GetInstanceJson: + case _OrthancPluginService_GetInstanceSimplifiedJson: + { + Json::StyledWriter writer; + std::string s; + + if (service == _OrthancPluginService_GetInstanceJson) + { + s = writer.write(instance.GetJson()); + } + else + { + Json::Value simplified; + SimplifyTags(simplified, instance.GetJson()); + s = writer.write(simplified); + } + + *p.resultStringToFree = CopyString(s); + return; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + bool OrthancPlugins::InvokeService(_OrthancPluginService service, + const void* parameters) + { + switch (service) + { + case _OrthancPluginService_GetOrthancPath: + { + std::string s = Toolbox::GetPathToExecutable(); + *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters)->result = CopyString(s); + return true; + } + + case _OrthancPluginService_GetOrthancDirectory: + { + std::string s = Toolbox::GetDirectoryOfExecutable(); + *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters)->result = CopyString(s); + return true; + } + + case _OrthancPluginService_GetConfigurationPath: + { + *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters)->result = + CopyString(Configuration::GetConfigurationAbsolutePath()); + return true; + } + + case _OrthancPluginService_RegisterRestCallback: + RegisterRestCallback(parameters); + return true; + + case _OrthancPluginService_RegisterOnStoredInstanceCallback: + RegisterOnStoredInstanceCallback(parameters); + return true; + + case _OrthancPluginService_RegisterOnChangeCallback: + RegisterOnChangeCallback(parameters); + return true; + + case _OrthancPluginService_AnswerBuffer: + AnswerBuffer(parameters); + return true; + + case _OrthancPluginService_CompressAndAnswerPngImage: + CompressAndAnswerPngImage(parameters); + return true; + + case _OrthancPluginService_GetDicomForInstance: + GetDicomForInstance(parameters); + return true; + + case _OrthancPluginService_RestApiGet: + RestApiGet(parameters, false); + return true; + + case _OrthancPluginService_RestApiGetAfterPlugins: + RestApiGet(parameters, true); + return true; + + case _OrthancPluginService_RestApiPost: + RestApiPostPut(true, parameters, false); + return true; + + case _OrthancPluginService_RestApiPostAfterPlugins: + RestApiPostPut(true, parameters, true); + return true; + + case _OrthancPluginService_RestApiDelete: + RestApiDelete(parameters, false); + return true; + + case _OrthancPluginService_RestApiDeleteAfterPlugins: + RestApiDelete(parameters, true); + return true; + + case _OrthancPluginService_RestApiPut: + RestApiPostPut(false, parameters, false); + return true; + + case _OrthancPluginService_RestApiPutAfterPlugins: + RestApiPostPut(false, parameters, true); + return true; + + case _OrthancPluginService_Redirect: + Redirect(parameters); + return true; + + case _OrthancPluginService_SendUnauthorized: + SendUnauthorized(parameters); + return true; + + case _OrthancPluginService_SendMethodNotAllowed: + SendMethodNotAllowed(parameters); + return true; + + case _OrthancPluginService_SendHttpStatusCode: + SendHttpStatusCode(parameters); + return true; + + case _OrthancPluginService_SetCookie: + SetCookie(parameters); + return true; + + case _OrthancPluginService_SetHttpHeader: + SetHttpHeader(parameters); + return true; + + case _OrthancPluginService_LookupPatient: + case _OrthancPluginService_LookupStudy: + case _OrthancPluginService_LookupStudyWithAccessionNumber: + case _OrthancPluginService_LookupSeries: + case _OrthancPluginService_LookupInstance: + LookupResource(service, parameters); + return true; + + case _OrthancPluginService_GetInstanceRemoteAet: + case _OrthancPluginService_GetInstanceSize: + case _OrthancPluginService_GetInstanceData: + case _OrthancPluginService_GetInstanceJson: + case _OrthancPluginService_GetInstanceSimplifiedJson: + case _OrthancPluginService_HasInstanceMetadata: + case _OrthancPluginService_GetInstanceMetadata: + AccessDicomInstance(service, parameters); + return true; + + case _OrthancPluginService_RegisterStorageArea: + { + LOG(INFO) << "Plugin has registered a custom storage area"; + const _OrthancPluginRegisterStorageArea& p = + *reinterpret_cast<const _OrthancPluginRegisterStorageArea*>(parameters); + + pimpl_->storageArea_ = p; + pimpl_->hasStorageArea_ = true; + return true; + } + + case _OrthancPluginService_SetPluginProperty: + { + const _OrthancPluginSetPluginProperty& p = + *reinterpret_cast<const _OrthancPluginSetPluginProperty*>(parameters); + pimpl_->properties_[std::make_pair(p.plugin, p.property)] = p.value; + return true; + } + + case _OrthancPluginService_SetGlobalProperty: + { + const _OrthancPluginGlobalProperty& p = + *reinterpret_cast<const _OrthancPluginGlobalProperty*>(parameters); + if (p.property < 1024) + { + return false; + } + else + { + assert(pimpl_->context_ != NULL); + pimpl_->context_->GetIndex().SetGlobalProperty(static_cast<GlobalProperty>(p.property), p.value); + return true; + } + } + + case _OrthancPluginService_GetGlobalProperty: + { + assert(pimpl_->context_ != NULL); + + const _OrthancPluginGlobalProperty& p = + *reinterpret_cast<const _OrthancPluginGlobalProperty*>(parameters); + std::string result = pimpl_->context_->GetIndex().GetGlobalProperty(static_cast<GlobalProperty>(p.property), p.value); + *(p.result) = CopyString(result); + return true; + } + + case _OrthancPluginService_GetCommandLineArgumentsCount: + { + const _OrthancPluginReturnSingleValue& p = + *reinterpret_cast<const _OrthancPluginReturnSingleValue*>(parameters); + *(p.resultUint32) = pimpl_->argc_ - 1; + return true; + } + + case _OrthancPluginService_GetCommandLineArgument: + { + const _OrthancPluginGlobalProperty& p = + *reinterpret_cast<const _OrthancPluginGlobalProperty*>(parameters); + + if (p.property + 1 > pimpl_->argc_) + { + return false; + } + else + { + std::string arg = std::string(pimpl_->argv_[p.property + 1]); + *(p.result) = CopyString(arg); + return true; + } + } + + case _OrthancPluginService_RegisterDatabaseBackend: + { + LOG(INFO) << "Plugin has registered a custom database back-end"; + const _OrthancPluginRegisterDatabaseBackend& p = + *reinterpret_cast<const _OrthancPluginRegisterDatabaseBackend*>(parameters); + + pimpl_->database_.reset(new OrthancPluginDatabase(*p.backend, p.payload)); + *(p.result) = reinterpret_cast<OrthancPluginDatabaseContext*>(pimpl_->database_.get()); + + return true; + } + + case _OrthancPluginService_DatabaseAnswer: + { + const _OrthancPluginDatabaseAnswer& p = + *reinterpret_cast<const _OrthancPluginDatabaseAnswer*>(parameters); + if (pimpl_->database_.get() != NULL) + { + pimpl_->database_->AnswerReceived(p); + return true; + } + else + { + LOG(ERROR) << "Cannot invoke this service without a custom database back-end"; + throw OrthancException(ErrorCode_BadRequest); + } + } + + default: + return false; + } + } + + + void OrthancPlugins::SetOrthancRestApi(OrthancRestApi& restApi) + { + pimpl_->restApi_ = &restApi; + } + + + void OrthancPlugins::ResetOrthancRestApi() + { + pimpl_->restApi_ = NULL; + } + + + bool OrthancPlugins::HasStorageArea() const + { + return pimpl_->hasStorageArea_; + } + + bool OrthancPlugins::HasDatabase() const + { + return pimpl_->database_.get() != NULL; + } + + + + namespace + { + class PluginStorageArea : public IStorageArea + { + private: + _OrthancPluginRegisterStorageArea params_; + + void Free(void* buffer) const + { + if (buffer != NULL) + { + params_.free(buffer); + } + } + + OrthancPluginContentType Convert(FileContentType type) const + { + switch (type) + { + case FileContentType_Dicom: + return OrthancPluginContentType_Dicom; + + case FileContentType_DicomAsJson: + return OrthancPluginContentType_DicomAsJson; + + default: + return OrthancPluginContentType_Unknown; + } + } + + public: + PluginStorageArea(const _OrthancPluginRegisterStorageArea& params) : params_(params) + { + } + + virtual void Create(const std::string& uuid, + const void* content, + size_t size, + FileContentType type) + { + if (params_.create(uuid.c_str(), content, size, Convert(type)) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + } + + virtual void Read(std::string& content, + const std::string& uuid, + FileContentType type) + { + void* buffer = NULL; + int64_t size = 0; + + if (params_.read(&buffer, &size, uuid.c_str(), Convert(type)) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + + try + { + content.resize(size); + } + catch (OrthancException&) + { + Free(buffer); + throw; + } + + if (size > 0) + { + memcpy(&content[0], buffer, size); + } + + Free(buffer); + } + + virtual void Remove(const std::string& uuid, + FileContentType type) + { + if (params_.remove(uuid.c_str(), Convert(type)) != 0) + { + throw OrthancException(ErrorCode_Plugin); + } + } + }; + } + + + IStorageArea* OrthancPlugins::GetStorageArea() + { + if (!HasStorageArea()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + return new PluginStorageArea(pimpl_->storageArea_); + } + + + IDatabaseWrapper& OrthancPlugins::GetDatabase() + { + if (!HasDatabase()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + return *pimpl_->database_; + } + + + + + + const char* OrthancPlugins::GetProperty(const char* plugin, + _OrthancPluginProperty property) const + { + PImpl::Property p = std::make_pair(plugin, property); + PImpl::Properties::const_iterator it = pimpl_->properties_.find(p); + + if (it == pimpl_->properties_.end()) + { + return NULL; + } + else + { + return it->second.c_str(); + } + } + + + void OrthancPlugins::SetCommandLineArguments(int argc, char* argv[]) + { + if (argc < 1 || argv == NULL) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + pimpl_->argc_ = argc; + pimpl_->argv_ = argv; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Engine/OrthancPlugins.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,130 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "PluginsManager.h" +#include "../../Core/HttpServer/HttpHandler.h" +#include "../../OrthancServer/ServerContext.h" +#include "../../OrthancServer/OrthancRestApi/OrthancRestApi.h" +#include "OrthancPluginDatabase.h" + +#include <list> +#include <boost/shared_ptr.hpp> + +namespace Orthanc +{ + class OrthancPlugins : public HttpHandler, public IPluginServiceProvider + { + private: + struct PImpl; + boost::shared_ptr<PImpl> pimpl_; + + void RegisterRestCallback(const void* parameters); + + void RegisterOnStoredInstanceCallback(const void* parameters); + + void RegisterOnChangeCallback(const void* parameters); + + void AnswerBuffer(const void* parameters); + + void Redirect(const void* parameters); + + void CompressAndAnswerPngImage(const void* parameters); + + void GetDicomForInstance(const void* parameters); + + void RestApiGet(const void* parameters, + bool afterPlugins); + + void RestApiPostPut(bool isPost, + const void* parameters, + bool afterPlugins); + + void RestApiDelete(const void* parameters, + bool afterPlugins); + + void LookupResource(_OrthancPluginService service, + const void* parameters); + + void SendHttpStatusCode(const void* parameters); + + void SendUnauthorized(const void* parameters); + + void SendMethodNotAllowed(const void* parameters); + + void SetCookie(const void* parameters); + + void SetHttpHeader(const void* parameters); + + public: + OrthancPlugins(); + + virtual ~OrthancPlugins(); + + void SetServerContext(ServerContext& context); + + virtual bool Handle(HttpOutput& output, + HttpMethod method, + const UriComponents& uri, + const Arguments& headers, + const GetArguments& getArguments, + const std::string& postData); + + virtual bool InvokeService(_OrthancPluginService service, + const void* parameters); + + void SignalChange(const ServerIndexChange& change); + + void SignalStoredInstance(DicomInstanceToStore& instance, + const std::string& instanceId); + + void SetOrthancRestApi(OrthancRestApi& restApi); + + void ResetOrthancRestApi(); + + bool HasStorageArea() const; + + IStorageArea* GetStorageArea(); // To be freed after use + + bool HasDatabase() const; + + IDatabaseWrapper& GetDatabase(); + + void Stop(); + + const char* GetProperty(const char* plugin, + _OrthancPluginProperty property) const; + + void SetCommandLineArguments(int argc, char* argv[]); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Engine/PluginsManager.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,344 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "PluginsManager.h" + +#include "../../Core/Toolbox.h" +#include "../../Core/HttpServer/HttpOutput.h" + +#include <glog/logging.h> +#include <cassert> +#include <memory> +#include <boost/filesystem.hpp> + +#ifdef WIN32 +#define PLUGIN_EXTENSION ".dll" +#elif defined(__linux) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) +#define PLUGIN_EXTENSION ".so" +#elif defined(__APPLE__) && defined(__MACH__) +#define PLUGIN_EXTENSION ".dylib" +#else +#error Support your platform here +#endif + + +namespace Orthanc +{ + static void CallInitialize(SharedLibrary& plugin, + const OrthancPluginContext& context) + { + typedef int32_t (*Initialize) (const OrthancPluginContext*); + +#if defined(_WIN32) + Initialize initialize = (Initialize) plugin.GetFunction("OrthancPluginInitialize"); +#else + /** + * gcc would complain about "ISO C++ forbids casting between + * pointer-to-function and pointer-to-object" without the trick + * below, that is known as "the POSIX.1-2003 (Technical Corrigendum + * 1) workaround". See the man page of "dlsym()". + * http://www.trilithium.com/johan/2004/12/problem-with-dlsym/ + * http://stackoverflow.com/a/14543811/881731 + **/ + + Initialize initialize; + *(void **) (&initialize) = plugin.GetFunction("OrthancPluginInitialize"); +#endif + + assert(initialize != NULL); + int32_t error = initialize(&context); + + if (error != 0) + { + LOG(ERROR) << "Error while initializing plugin " << plugin.GetPath() + << " (code " << error << ")"; + throw OrthancException(ErrorCode_SharedLibrary); + } + } + + + static void CallFinalize(SharedLibrary& plugin) + { + typedef void (*Finalize) (); + +#if defined(_WIN32) + Finalize finalize = (Finalize) plugin.GetFunction("OrthancPluginFinalize"); +#else + Finalize finalize; + *(void **) (&finalize) = plugin.GetFunction("OrthancPluginFinalize"); +#endif + + assert(finalize != NULL); + finalize(); + } + + + static const char* CallGetName(SharedLibrary& plugin) + { + typedef const char* (*GetName) (); + +#if defined(_WIN32) + GetName getName = (GetName) plugin.GetFunction("OrthancPluginGetName"); +#else + GetName getName; + *(void **) (&getName) = plugin.GetFunction("OrthancPluginGetName"); +#endif + + assert(getName != NULL); + return getName(); + } + + + static const char* CallGetVersion(SharedLibrary& plugin) + { + typedef const char* (*GetVersion) (); + +#if defined(_WIN32) + GetVersion getVersion = (GetVersion) plugin.GetFunction("OrthancPluginGetVersion"); +#else + GetVersion getVersion; + *(void **) (&getVersion) = plugin.GetFunction("OrthancPluginGetVersion"); +#endif + + assert(getVersion != NULL); + return getVersion(); + } + + + int32_t PluginsManager::InvokeService(OrthancPluginContext* context, + _OrthancPluginService service, + const void* params) + { + switch (service) + { + case _OrthancPluginService_LogError: + LOG(ERROR) << reinterpret_cast<const char*>(params); + return 0; + + case _OrthancPluginService_LogWarning: + LOG(WARNING) << reinterpret_cast<const char*>(params); + return 0; + + case _OrthancPluginService_LogInfo: + LOG(INFO) << reinterpret_cast<const char*>(params); + return 0; + + default: + break; + } + + PluginsManager* that = reinterpret_cast<PluginsManager*>(context->pluginsManager); + bool error = false; + + for (std::list<IPluginServiceProvider*>::iterator + it = that->serviceProviders_.begin(); + it != that->serviceProviders_.end(); ++it) + { + try + { + if ((*it)->InvokeService(service, params)) + { + return 0; + } + } + catch (OrthancException&) + { + // This service provider has failed, go to the next + error = true; + } + } + + if (error) + { + // LOG(ERROR) << "Exception when dealing with service " << service; + } + else + { + LOG(ERROR) << "Plugin invoking unknown service " << service; + } + + return -1; + } + + + PluginsManager::PluginsManager() + { + memset(&context_, 0, sizeof(context_)); + context_.pluginsManager = this; + context_.orthancVersion = ORTHANC_VERSION; + context_.Free = ::free; + context_.InvokeService = InvokeService; + } + + PluginsManager::~PluginsManager() + { + for (Plugins::iterator it = plugins_.begin(); it != plugins_.end(); ++it) + { + if (it->second != NULL) + { + LOG(WARNING) << "Unregistering plugin '" << it->first + << "' (version " << it->second->GetVersion() << ")"; + + CallFinalize(it->second->GetLibrary()); + delete it->second; + } + } + } + + + static bool IsOrthancPlugin(SharedLibrary& library) + { + return (library.HasFunction("OrthancPluginInitialize") && + library.HasFunction("OrthancPluginFinalize") && + library.HasFunction("OrthancPluginGetName") && + library.HasFunction("OrthancPluginGetVersion")); + } + + + void PluginsManager::RegisterPlugin(const std::string& path) + { + if (!boost::filesystem::exists(path)) + { + LOG(ERROR) << "Inexistent path to plugins: " << path; + return; + } + + if (boost::filesystem::is_directory(path)) + { + ScanFolderForPlugins(path, false); + return; + } + + std::auto_ptr<Plugin> plugin(new Plugin(path)); + + if (!IsOrthancPlugin(plugin->GetLibrary())) + { + LOG(ERROR) << "Plugin " << plugin->GetLibrary().GetPath() + << " does not declare the proper entry functions"; + throw OrthancException(ErrorCode_SharedLibrary); + } + + std::string name(CallGetName(plugin->GetLibrary())); + if (plugins_.find(name) != plugins_.end()) + { + LOG(ERROR) << "Plugin '" << name << "' already registered"; + throw OrthancException(ErrorCode_SharedLibrary); + } + + plugin->SetVersion(CallGetVersion(plugin->GetLibrary())); + LOG(WARNING) << "Registering plugin '" << name + << "' (version " << plugin->GetVersion() << ")"; + + CallInitialize(plugin->GetLibrary(), context_); + + plugins_[name] = plugin.release(); + } + + + void PluginsManager::ScanFolderForPlugins(const std::string& folder, + bool isRecursive) + { + using namespace boost::filesystem; + + if (!exists(folder)) + { + return; + } + + LOG(INFO) << "Scanning folder " << folder << " for plugins"; + + directory_iterator end_it; // default construction yields past-the-end + for (directory_iterator it(folder); + it != end_it; + ++it) + { + std::string path = it->path().string(); + + if (is_directory(it->status())) + { + if (isRecursive) + { + ScanFolderForPlugins(path, true); + } + } + else + { + std::string extension = boost::filesystem::extension(it->path()); + Toolbox::ToLowerCase(extension); + + if (extension == PLUGIN_EXTENSION) + { + LOG(INFO) << "Found a shared library: " << it->path(); + + SharedLibrary plugin(path); + if (IsOrthancPlugin(plugin)) + { + RegisterPlugin(path); + } + } + } + } + } + + + void PluginsManager::ListPlugins(std::list<std::string>& result) const + { + result.clear(); + + for (Plugins::const_iterator it = plugins_.begin(); + it != plugins_.end(); ++it) + { + result.push_back(it->first); + } + } + + + bool PluginsManager::HasPlugin(const std::string& name) const + { + return plugins_.find(name) != plugins_.end(); + } + + + const std::string& PluginsManager::GetPluginVersion(const std::string& name) const + { + Plugins::const_iterator it = plugins_.find(name); + if (it == plugins_.end()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + return it->second->GetVersion(); + } + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Engine/PluginsManager.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,104 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "SharedLibrary.h" +#include "IPluginServiceProvider.h" + +#include <map> +#include <list> + +namespace Orthanc +{ + class PluginsManager : boost::noncopyable + { + private: + class Plugin + { + private: + SharedLibrary library_; + std::string version_; + + public: + Plugin(const std::string& path) : library_(path) + { + } + + SharedLibrary& GetLibrary() + { + return library_; + } + + void SetVersion(const std::string& version) + { + version_ = version; + } + + const std::string& GetVersion() const + { + return version_; + } + }; + + typedef std::map<std::string, Plugin*> Plugins; + + OrthancPluginContext context_; + Plugins plugins_; + std::list<IPluginServiceProvider*> serviceProviders_; + + static int32_t InvokeService(OrthancPluginContext* context, + _OrthancPluginService service, + const void* parameters); + + public: + PluginsManager(); + + ~PluginsManager(); + + void RegisterPlugin(const std::string& path); + + void ScanFolderForPlugins(const std::string& path, + bool isRecursive); + + void RegisterServiceProvider(IPluginServiceProvider& provider) + { + serviceProviders_.push_back(&provider); + } + + void ListPlugins(std::list<std::string>& result) const; + + bool HasPlugin(const std::string& name) const; + + const std::string& GetPluginVersion(const std::string& name) const; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Engine/SharedLibrary.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,133 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "SharedLibrary.h" + +#include "../../Core/Toolbox.h" + +#if defined(_WIN32) +#include <windows.h> +#elif defined(__linux) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) +#include <dlfcn.h> +#else +#error Support your platform here +#endif + +#include <glog/logging.h> + +namespace Orthanc +{ + SharedLibrary::SharedLibrary(const std::string& path) : + path_(path), + handle_(NULL) + { +#if defined(_WIN32) + handle_ = ::LoadLibraryA(path.c_str()); + if (handle_ == NULL) + { + LOG(ERROR) << "LoadLibrary(" << path << ") failed: Error " << ::GetLastError(); + throw OrthancException(ErrorCode_SharedLibrary); + } + +#elif defined(__linux) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) + handle_ = ::dlopen(path.c_str(), RTLD_NOW); + if (handle_ == NULL) + { + std::string explanation; + const char *tmp = ::dlerror(); + if (tmp) + { + explanation = ": Error " + std::string(tmp); + } + + LOG(ERROR) << "dlopen(" << path << ") failed" << explanation; + throw OrthancException(ErrorCode_SharedLibrary); + } + +#else +#error Support your platform here +#endif + } + + SharedLibrary::~SharedLibrary() + { + if (handle_) + { +#if defined(_WIN32) + ::FreeLibrary((HMODULE)handle_); +#elif defined(__linux) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) + ::dlclose(handle_); +#else +#error Support your platform here +#endif + } + } + + + SharedLibrary::FunctionPointer SharedLibrary::GetFunctionInternal(const std::string& name) + { + if (!handle_) + { + throw OrthancException(ErrorCode_InternalError); + } + +#if defined(_WIN32) + return ::GetProcAddress((HMODULE)handle_, name.c_str()); +#elif defined(__linux) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) + return ::dlsym(handle_, name.c_str()); +#else +#error Support your platform here +#endif + } + + + SharedLibrary::FunctionPointer SharedLibrary::GetFunction(const std::string& name) + { + SharedLibrary::FunctionPointer result = GetFunctionInternal(name); + + if (result == NULL) + { + LOG(ERROR) << "Shared library does not expose function \"" << name << "\""; + throw OrthancException(ErrorCode_SharedLibrary); + } + else + { + return result; + } + } + + + bool SharedLibrary::HasFunction(const std::string& name) + { + return GetFunctionInternal(name) != NULL; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Engine/SharedLibrary.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,70 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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/OrthancException.h" + +#include <boost/noncopyable.hpp> + +namespace Orthanc +{ + class SharedLibrary : boost::noncopyable + { + public: +#if defined(_WIN32) + typedef FARPROC FunctionPointer; +#else + typedef void* FunctionPointer; +#endif + + private: + std::string path_; + void *handle_; + + FunctionPointer GetFunctionInternal(const std::string& name); + + public: + SharedLibrary(const std::string& path); + + ~SharedLibrary(); + + const std::string& GetPath() const + { + return path_; + } + + bool HasFunction(const std::string& name); + + FunctionPointer GetFunction(const std::string& name); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Include/OrthancCDatabasePlugin.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,664 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "OrthancCPlugin.h" + + +/** @{ */ + +#ifdef __cplusplus +extern "C" +{ +#endif + + typedef struct _OrthancPluginDatabaseContext_t OrthancPluginDatabaseContext; + + + typedef enum + { + _OrthancPluginDatabaseAnswerType_None = 0, + + /* Events */ + _OrthancPluginDatabaseAnswerType_DeletedAttachment = 1, + _OrthancPluginDatabaseAnswerType_DeletedResource = 2, + _OrthancPluginDatabaseAnswerType_RemainingAncestor = 3, + + /* Return value */ + _OrthancPluginDatabaseAnswerType_Attachment = 10, + _OrthancPluginDatabaseAnswerType_Change = 11, + _OrthancPluginDatabaseAnswerType_DicomTag = 12, + _OrthancPluginDatabaseAnswerType_ExportedResource = 13, + _OrthancPluginDatabaseAnswerType_Int32 = 14, + _OrthancPluginDatabaseAnswerType_Int64 = 15, + _OrthancPluginDatabaseAnswerType_Resource = 16, + _OrthancPluginDatabaseAnswerType_String = 17 + } _OrthancPluginDatabaseAnswerType; + + + typedef struct + { + const char* uuid; + int32_t contentType; + uint64_t uncompressedSize; + const char* uncompressedHash; + int32_t compressionType; + uint64_t compressedSize; + const char* compressedHash; + } OrthancPluginAttachment; + + typedef struct + { + uint16_t group; + uint16_t element; + const char* value; + } OrthancPluginDicomTag; + + typedef struct + { + int64_t seq; + int32_t changeType; + OrthancPluginResourceType resourceType; + const char* publicId; + const char* date; + } OrthancPluginChange; + + typedef struct + { + int64_t seq; + OrthancPluginResourceType resourceType; + const char* publicId; + const char* modality; + const char* date; + const char* patientId; + const char* studyInstanceUid; + const char* seriesInstanceUid; + const char* sopInstanceUid; + } OrthancPluginExportedResource; + + + typedef struct + { + OrthancPluginDatabaseContext* database; + _OrthancPluginDatabaseAnswerType type; + int32_t valueInt32; + uint32_t valueUint32; + int64_t valueInt64; + const char *valueString; + const void *valueGeneric; + } _OrthancPluginDatabaseAnswer; + + ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerString( + OrthancPluginContext* context, + OrthancPluginDatabaseContext* database, + const char* value) + { + _OrthancPluginDatabaseAnswer params; + memset(¶ms, 0, sizeof(params)); + params.database = database; + params.type = _OrthancPluginDatabaseAnswerType_String; + params.valueString = value; + context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, ¶ms); + } + + ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerChange( + OrthancPluginContext* context, + OrthancPluginDatabaseContext* database, + const OrthancPluginChange* change) + { + _OrthancPluginDatabaseAnswer params; + memset(¶ms, 0, sizeof(params)); + + params.database = database; + params.type = _OrthancPluginDatabaseAnswerType_Change; + params.valueUint32 = 0; + params.valueGeneric = change; + + context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, ¶ms); + } + + ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerChangesDone( + OrthancPluginContext* context, + OrthancPluginDatabaseContext* database) + { + _OrthancPluginDatabaseAnswer params; + memset(¶ms, 0, sizeof(params)); + + params.database = database; + params.type = _OrthancPluginDatabaseAnswerType_Change; + params.valueUint32 = 1; + params.valueGeneric = NULL; + + context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, ¶ms); + } + + ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerInt32( + OrthancPluginContext* context, + OrthancPluginDatabaseContext* database, + int32_t value) + { + _OrthancPluginDatabaseAnswer params; + memset(¶ms, 0, sizeof(params)); + params.database = database; + params.type = _OrthancPluginDatabaseAnswerType_Int32; + params.valueInt32 = value; + context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, ¶ms); + } + + ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerInt64( + OrthancPluginContext* context, + OrthancPluginDatabaseContext* database, + int64_t value) + { + _OrthancPluginDatabaseAnswer params; + memset(¶ms, 0, sizeof(params)); + params.database = database; + params.type = _OrthancPluginDatabaseAnswerType_Int64; + params.valueInt64 = value; + context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, ¶ms); + } + + ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerExportedResource( + OrthancPluginContext* context, + OrthancPluginDatabaseContext* database, + const OrthancPluginExportedResource* exported) + { + _OrthancPluginDatabaseAnswer params; + memset(¶ms, 0, sizeof(params)); + + params.database = database; + params.type = _OrthancPluginDatabaseAnswerType_ExportedResource; + params.valueUint32 = 0; + params.valueGeneric = exported; + context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, ¶ms); + } + + ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerExportedResourcesDone( + OrthancPluginContext* context, + OrthancPluginDatabaseContext* database) + { + _OrthancPluginDatabaseAnswer params; + memset(¶ms, 0, sizeof(params)); + + params.database = database; + params.type = _OrthancPluginDatabaseAnswerType_ExportedResource; + params.valueUint32 = 1; + params.valueGeneric = NULL; + context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, ¶ms); + } + + ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerDicomTag( + OrthancPluginContext* context, + OrthancPluginDatabaseContext* database, + const OrthancPluginDicomTag* tag) + { + _OrthancPluginDatabaseAnswer params; + memset(¶ms, 0, sizeof(params)); + params.database = database; + params.type = _OrthancPluginDatabaseAnswerType_DicomTag; + params.valueGeneric = tag; + context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, ¶ms); + } + + ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerAttachment( + OrthancPluginContext* context, + OrthancPluginDatabaseContext* database, + const OrthancPluginAttachment* attachment) + { + _OrthancPluginDatabaseAnswer params; + memset(¶ms, 0, sizeof(params)); + params.database = database; + params.type = _OrthancPluginDatabaseAnswerType_Attachment; + params.valueGeneric = attachment; + context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, ¶ms); + } + + ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerResource( + OrthancPluginContext* context, + OrthancPluginDatabaseContext* database, + int64_t id, + OrthancPluginResourceType resourceType) + { + _OrthancPluginDatabaseAnswer params; + memset(¶ms, 0, sizeof(params)); + params.database = database; + params.type = _OrthancPluginDatabaseAnswerType_Resource; + params.valueInt64 = id; + params.valueInt32 = (int32_t) resourceType; + context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, ¶ms); + } + + ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseSignalDeletedAttachment( + OrthancPluginContext* context, + OrthancPluginDatabaseContext* database, + const OrthancPluginAttachment* attachment) + { + _OrthancPluginDatabaseAnswer params; + memset(¶ms, 0, sizeof(params)); + params.database = database; + params.type = _OrthancPluginDatabaseAnswerType_DeletedAttachment; + params.valueGeneric = attachment; + context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, ¶ms); + } + + ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseSignalDeletedResource( + OrthancPluginContext* context, + OrthancPluginDatabaseContext* database, + const char* publicId, + OrthancPluginResourceType resourceType) + { + _OrthancPluginDatabaseAnswer params; + memset(¶ms, 0, sizeof(params)); + params.database = database; + params.type = _OrthancPluginDatabaseAnswerType_DeletedResource; + params.valueString = publicId; + params.valueInt32 = (int32_t) resourceType; + context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, ¶ms); + } + + ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseSignalRemainingAncestor( + OrthancPluginContext* context, + OrthancPluginDatabaseContext* database, + const char* ancestorId, + OrthancPluginResourceType ancestorType) + { + _OrthancPluginDatabaseAnswer params; + memset(¶ms, 0, sizeof(params)); + params.database = database; + params.type = _OrthancPluginDatabaseAnswerType_RemainingAncestor; + params.valueString = ancestorId; + params.valueInt32 = (int32_t) ancestorType; + context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, ¶ms); + } + + + + + + typedef struct + { + int32_t (*addAttachment) ( + /* inputs */ + void* payload, + int64_t id, + const OrthancPluginAttachment* attachment); + + int32_t (*attachChild) ( + /* inputs */ + void* payload, + int64_t parent, + int64_t child); + + int32_t (*clearChanges) ( + /* inputs */ + void* payload); + + int32_t (*clearExportedResources) ( + /* inputs */ + void* payload); + + int32_t (*createResource) ( + /* outputs */ + int64_t* id, + /* inputs */ + void* payload, + const char* publicId, + OrthancPluginResourceType resourceType); + + int32_t (*deleteAttachment) ( + /* inputs */ + void* payload, + int64_t id, + int32_t contentType); + + int32_t (*deleteMetadata) ( + /* inputs */ + void* payload, + int64_t id, + int32_t metadataType); + + int32_t (*deleteResource) ( + /* inputs */ + void* payload, + int64_t id); + + /* Output: Use OrthancPluginDatabaseAnswerString() */ + int32_t (*getAllPublicIds) ( + /* outputs */ + OrthancPluginDatabaseContext* context, + /* inputs */ + void* payload, + OrthancPluginResourceType resourceType); + + /* Output: Use OrthancPluginDatabaseAnswerChange() and + * OrthancPluginDatabaseAnswerChangesDone() */ + int32_t (*getChanges) ( + /* outputs */ + OrthancPluginDatabaseContext* context, + /* inputs */ + void* payload, + int64_t since, + uint32_t maxResult); + + /* Output: Use OrthancPluginDatabaseAnswerInt64() */ + int32_t (*getChildrenInternalId) ( + /* outputs */ + OrthancPluginDatabaseContext* context, + /* inputs */ + void* payload, + int64_t id); + + /* Output: Use OrthancPluginDatabaseAnswerString() */ + int32_t (*getChildrenPublicId) ( + /* outputs */ + OrthancPluginDatabaseContext* context, + /* inputs */ + void* payload, + int64_t id); + + /* Output: Use OrthancPluginDatabaseAnswerExportedResource() and + * OrthancPluginDatabaseAnswerExportedResourcesDone() */ + int32_t (*getExportedResources) ( + /* outputs */ + OrthancPluginDatabaseContext* context, + /* inputs */ + void* payload, + int64_t since, + uint32_t maxResult); + + /* Output: Use OrthancPluginDatabaseAnswerChange() */ + int32_t (*getLastChange) ( + /* outputs */ + OrthancPluginDatabaseContext* context, + /* inputs */ + void* payload); + + /* Output: Use OrthancPluginDatabaseAnswerExportedResource() */ + int32_t (*getLastExportedResource) ( + /* outputs */ + OrthancPluginDatabaseContext* context, + /* inputs */ + void* payload); + + /* Output: Use OrthancPluginDatabaseAnswerDicomTag() */ + int32_t (*getMainDicomTags) ( + /* outputs */ + OrthancPluginDatabaseContext* context, + /* inputs */ + void* payload, + int64_t id); + + /* Output: Use OrthancPluginDatabaseAnswerString() */ + int32_t (*getPublicId) ( + /* outputs */ + OrthancPluginDatabaseContext* context, + /* inputs */ + void* payload, + int64_t id); + + int32_t (*getResourceCount) ( + /* outputs */ + uint64_t* target, + /* inputs */ + void* payload, + OrthancPluginResourceType resourceType); + + int32_t (*getResourceType) ( + /* outputs */ + OrthancPluginResourceType* resourceType, + /* inputs */ + void* payload, + int64_t id); + + int32_t (*getTotalCompressedSize) ( + /* outputs */ + uint64_t* target, + /* inputs */ + void* payload); + + int32_t (*getTotalUncompressedSize) ( + /* outputs */ + uint64_t* target, + /* inputs */ + void* payload); + + int32_t (*isExistingResource) ( + /* outputs */ + int32_t* existing, + /* inputs */ + void* payload, + int64_t id); + + int32_t (*isProtectedPatient) ( + /* outputs */ + int32_t* isProtected, + /* inputs */ + void* payload, + int64_t id); + + /* Output: Use OrthancPluginDatabaseAnswerInt32() */ + int32_t (*listAvailableMetadata) ( + /* outputs */ + OrthancPluginDatabaseContext* context, + /* inputs */ + void* payload, + int64_t id); + + /* Output: Use OrthancPluginDatabaseAnswerInt32() */ + int32_t (*listAvailableAttachments) ( + /* outputs */ + OrthancPluginDatabaseContext* context, + /* inputs */ + void* payload, + int64_t id); + + int32_t (*logChange) ( + /* inputs */ + void* payload, + const OrthancPluginChange* change); + + int32_t (*logExportedResource) ( + /* inputs */ + void* payload, + const OrthancPluginExportedResource* exported); + + /* Output: Use OrthancPluginDatabaseAnswerAttachment() */ + int32_t (*lookupAttachment) ( + /* outputs */ + OrthancPluginDatabaseContext* context, + /* inputs */ + void* payload, + int64_t id, + int32_t contentType); + + /* Output: Use OrthancPluginDatabaseAnswerString() */ + int32_t (*lookupGlobalProperty) ( + /* outputs */ + OrthancPluginDatabaseContext* context, + /* inputs */ + void* payload, + int32_t property); + + /* Output: Use OrthancPluginDatabaseAnswerInt64() */ + int32_t (*lookupIdentifier) ( + /* outputs */ + OrthancPluginDatabaseContext* context, + /* inputs */ + void* payload, + const OrthancPluginDicomTag* tag); + + /* Output: Use OrthancPluginDatabaseAnswerInt64() */ + int32_t (*lookupIdentifier2) ( + /* outputs */ + OrthancPluginDatabaseContext* context, + /* inputs */ + void* payload, + const char* value); + + /* Output: Use OrthancPluginDatabaseAnswerString() */ + int32_t (*lookupMetadata) ( + /* outputs */ + OrthancPluginDatabaseContext* context, + /* inputs */ + void* payload, + int64_t id, + int32_t metadata); + + /* Output: Use OrthancPluginDatabaseAnswerInt64() */ + int32_t (*lookupParent) ( + /* outputs */ + OrthancPluginDatabaseContext* context, + /* inputs */ + void* payload, + int64_t id); + + /* Output: Use OrthancPluginDatabaseAnswerResource() */ + int32_t (*lookupResource) ( + /* outputs */ + OrthancPluginDatabaseContext* context, + /* inputs */ + void* payload, + const char* publicId); + + /* Output: Use OrthancPluginDatabaseAnswerInt64() */ + int32_t (*selectPatientToRecycle) ( + /* outputs */ + OrthancPluginDatabaseContext* context, + /* inputs */ + void* payload); + + /* Output: Use OrthancPluginDatabaseAnswerInt64() */ + int32_t (*selectPatientToRecycle2) ( + /* outputs */ + OrthancPluginDatabaseContext* context, + /* inputs */ + void* payload, + int64_t patientIdToAvoid); + + int32_t (*setGlobalProperty) ( + /* inputs */ + void* payload, + int32_t property, + const char* value); + + int32_t (*setMainDicomTag) ( + /* inputs */ + void* payload, + int64_t id, + const OrthancPluginDicomTag* tag); + + int32_t (*setIdentifierTag) ( + /* inputs */ + void* payload, + int64_t id, + const OrthancPluginDicomTag* tag); + + int32_t (*setMetadata) ( + /* inputs */ + void* payload, + int64_t id, + int32_t metadata, + const char* value); + + int32_t (*setProtectedPatient) ( + /* inputs */ + void* payload, + int64_t id, + int32_t isProtected); + + int32_t (*startTransaction) ( + /* inputs */ + void* payload); + + int32_t (*rollbackTransaction) ( + /* inputs */ + void* payload); + + int32_t (*commitTransaction) ( + /* inputs */ + void* payload); + + int32_t (*open) ( + /* inputs */ + void* payload); + + int32_t (*close) ( + /* inputs */ + void* payload); + + } OrthancPluginDatabaseBackend; + + + + typedef struct + { + OrthancPluginDatabaseContext** result; + const OrthancPluginDatabaseBackend* backend; + void* payload; + } _OrthancPluginRegisterDatabaseBackend; + + ORTHANC_PLUGIN_INLINE OrthancPluginDatabaseContext* OrthancPluginRegisterDatabaseBackend( + OrthancPluginContext* context, + const OrthancPluginDatabaseBackend* backend, + void* payload) + { + OrthancPluginDatabaseContext* result = NULL; + + _OrthancPluginRegisterDatabaseBackend params; + memset(¶ms, 0, sizeof(params)); + params.backend = backend; + params.result = &result; + params.payload = payload; + + if (context->InvokeService(context, _OrthancPluginService_RegisterDatabaseBackend, ¶ms) || + result == NULL) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + +#ifdef __cplusplus +} +#endif + + +/** @} */ +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Include/OrthancCPlugin.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,2094 @@ +/** + * \mainpage + * + * This C/C++ SDK allows external developers to create plugins that + * can be loaded into Orthanc to extend its functionality. Each + * Orthanc plugin must expose 4 public functions with the following + * signatures: + * + * -# <tt>int32_t OrthancPluginInitialize(const OrthancPluginContext* context)</tt>: + * This function is invoked by Orthanc when it loads the plugin on startup. + * The plugin must: + * - Check its compatibility with the Orthanc version using + * ::OrthancPluginCheckVersion(). + * - Store the context pointer so that it can use the plugin + * services of Orthanc. + * - Register all its REST callbacks using ::OrthancPluginRegisterRestCallback(). + * - Register all its callbacks for received instances using ::OrthancPluginRegisterOnStoredInstanceCallback(). + * - Possibly register a custom storage area using ::OrthancPluginRegisterStorageArea(). + * - Possibly register a custom database back-end area using ::OrthancPluginRegisterDatabaseBackend(). + * -# <tt>void OrthancPluginFinalize()</tt>: + * This function is invoked by Orthanc during its shutdown. The plugin + * must free all its memory. + * -# <tt>const char* OrthancPluginGetName()</tt>: + * The plugin must return a short string to identify itself. + * -# <tt>const char* OrthancPluginGetVersion()</tt>: + * The plugin must return a string containing its version number. + * + * The name and the version of a plugin is only used to prevent it + * from being loaded twice. + * + * The various callbacks are guaranteed to be executed in mutual + * exclusion since Orthanc 0.8.5. + **/ + + + +/** + * @defgroup CInterface C Interface + * @brief The C interface to create Orthanc plugins. + * + * These functions must be used to create C plugins for Orthanc. + **/ + + + +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 <stdio.h> +#include <string.h> + +#ifdef WIN32 +#define ORTHANC_PLUGINS_API __declspec(dllexport) +#else +#define ORTHANC_PLUGINS_API +#endif + +#define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER 0 +#define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER 8 +#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 6 + + + +/******************************************************************** + ** Check that function inlining is properly supported. The use of + ** inlining is required, to avoid the duplication of object code + ** between two compilation modules that would use the Orthanc Plugin + ** API. + ********************************************************************/ + +/* If the auto-detection of the "inline" keyword below does not work + automatically and that your compiler is known to properly support + inlining, uncomment the following #define and adapt the definition + of "static inline". */ + +/* #define ORTHANC_PLUGIN_INLINE static inline */ + +#ifndef ORTHANC_PLUGIN_INLINE +# if __STDC_VERSION__ >= 199901L +/* This is C99 or above: http://predef.sourceforge.net/prestd.html */ +# define ORTHANC_PLUGIN_INLINE static inline +# elif defined(__cplusplus) +/* This is C++ */ +# define ORTHANC_PLUGIN_INLINE static inline +# elif defined(__GNUC__) +/* This is GCC running in C89 mode */ +# define ORTHANC_PLUGIN_INLINE static __inline +# elif defined(_MSC_VER) +/* This is Visual Studio running in C89 mode */ +# define ORTHANC_PLUGIN_INLINE static __inline +# else +# error Your compiler is not known to support the "inline" keyword +# endif +#endif + + + +/******************************************************************** + ** Inclusion of standard libraries. + ********************************************************************/ + +/** + * For Microsoft Visual Studio, a compatibility "stdint.h" can be + * downloaded at the following URL: + * https://orthanc.googlecode.com/hg/Resources/ThirdParty/VisualStudio/stdint.h + **/ +#include <stdint.h> + +#include <stdlib.h> + + + +/******************************************************************** + ** Definition of the Orthanc Plugin API. + ********************************************************************/ + +/** @{ */ + +#ifdef __cplusplus +extern "C" +{ +#endif + + /** + * Forward declaration of one of the mandatory functions for Orthanc + * plugins. + **/ + ORTHANC_PLUGINS_API const char* OrthancPluginGetName(); + + + /** + * The various HTTP methods for a REST call. + **/ + typedef enum + { + OrthancPluginHttpMethod_Get = 1, /*!< GET request */ + OrthancPluginHttpMethod_Post = 2, /*!< POST request */ + OrthancPluginHttpMethod_Put = 3, /*!< PUT request */ + OrthancPluginHttpMethod_Delete = 4 /*!< DELETE request */ + } OrthancPluginHttpMethod; + + + /** + * @brief The parameters of a REST request. + **/ + typedef struct + { + /** + * @brief The HTTP method. + **/ + OrthancPluginHttpMethod method; + + /** + * @brief The number of groups of the regular expression. + **/ + uint32_t groupsCount; + + /** + * @brief The matched values for the groups of the regular expression. + **/ + const char* const* groups; + + /** + * @brief For a GET request, the number of GET parameters. + **/ + uint32_t getCount; + + /** + * @brief For a GET request, the keys of the GET parameters. + **/ + const char* const* getKeys; + + /** + * @brief For a GET request, the values of the GET parameters. + **/ + const char* const* getValues; + + /** + * @brief For a PUT or POST request, the content of the body. + **/ + const char* body; + + /** + * @brief For a PUT or POST request, the number of bytes of the body. + **/ + uint32_t bodySize; + + + /* -------------------------------------------------- + New in version 0.8.1 + -------------------------------------------------- */ + + /** + * @brief The number of HTTP headers. + **/ + uint32_t headersCount; + + /** + * @brief The keys of the HTTP headers (always converted to low-case). + **/ + const char* const* headersKeys; + + /** + * @brief The values of the HTTP headers. + **/ + const char* const* headersValues; + + } OrthancPluginHttpRequest; + + + typedef enum + { + /* Generic services */ + _OrthancPluginService_LogInfo = 1, + _OrthancPluginService_LogWarning = 2, + _OrthancPluginService_LogError = 3, + _OrthancPluginService_GetOrthancPath = 4, + _OrthancPluginService_GetOrthancDirectory = 5, + _OrthancPluginService_GetConfigurationPath = 6, + _OrthancPluginService_SetPluginProperty = 7, + _OrthancPluginService_GetGlobalProperty = 8, + _OrthancPluginService_SetGlobalProperty = 9, + _OrthancPluginService_GetCommandLineArgumentsCount = 10, + _OrthancPluginService_GetCommandLineArgument = 11, + + /* Registration of callbacks */ + _OrthancPluginService_RegisterRestCallback = 1000, + _OrthancPluginService_RegisterOnStoredInstanceCallback = 1001, + _OrthancPluginService_RegisterStorageArea = 1002, + _OrthancPluginService_RegisterOnChangeCallback = 1003, + + /* Sending answers to REST calls */ + _OrthancPluginService_AnswerBuffer = 2000, + _OrthancPluginService_CompressAndAnswerPngImage = 2001, + _OrthancPluginService_Redirect = 2002, + _OrthancPluginService_SendHttpStatusCode = 2003, + _OrthancPluginService_SendUnauthorized = 2004, + _OrthancPluginService_SendMethodNotAllowed = 2005, + _OrthancPluginService_SetCookie = 2006, + _OrthancPluginService_SetHttpHeader = 2007, + + /* Access to the Orthanc database and API */ + _OrthancPluginService_GetDicomForInstance = 3000, + _OrthancPluginService_RestApiGet = 3001, + _OrthancPluginService_RestApiPost = 3002, + _OrthancPluginService_RestApiDelete = 3003, + _OrthancPluginService_RestApiPut = 3004, + _OrthancPluginService_LookupPatient = 3005, + _OrthancPluginService_LookupStudy = 3006, + _OrthancPluginService_LookupSeries = 3007, + _OrthancPluginService_LookupInstance = 3008, + _OrthancPluginService_LookupStudyWithAccessionNumber = 3009, + _OrthancPluginService_RestApiGetAfterPlugins = 3010, + _OrthancPluginService_RestApiPostAfterPlugins = 3011, + _OrthancPluginService_RestApiDeleteAfterPlugins = 3012, + _OrthancPluginService_RestApiPutAfterPlugins = 3013, + + /* Access to DICOM instances */ + _OrthancPluginService_GetInstanceRemoteAet = 4000, + _OrthancPluginService_GetInstanceSize = 4001, + _OrthancPluginService_GetInstanceData = 4002, + _OrthancPluginService_GetInstanceJson = 4003, + _OrthancPluginService_GetInstanceSimplifiedJson = 4004, + _OrthancPluginService_HasInstanceMetadata = 4005, + _OrthancPluginService_GetInstanceMetadata = 4006, + + /* Services for plugins implementing a database back-end */ + _OrthancPluginService_RegisterDatabaseBackend = 5000, + _OrthancPluginService_DatabaseAnswer = 5001 + + } _OrthancPluginService; + + + typedef enum + { + _OrthancPluginProperty_Description = 1, + _OrthancPluginProperty_RootUri = 2, + _OrthancPluginProperty_OrthancExplorer = 3 + } _OrthancPluginProperty; + + + + /** + * The memory layout of the pixels of an image. + **/ + typedef enum + { + /** + * @brief Graylevel 8bpp image. + * + * The image is graylevel. Each pixel is unsigned and stored in + * one byte. + **/ + OrthancPluginPixelFormat_Grayscale8 = 1, + + /** + * @brief Graylevel, unsigned 16bpp image. + * + * The image is graylevel. Each pixel is unsigned and stored in + * two bytes. + **/ + OrthancPluginPixelFormat_Grayscale16 = 2, + + /** + * @brief Graylevel, signed 16bpp image. + * + * The image is graylevel. Each pixel is signed and stored in two + * bytes. + **/ + OrthancPluginPixelFormat_SignedGrayscale16 = 3, + + /** + * @brief Color image in RGB24 format. + * + * This format describes a color image. The pixels are stored in 3 + * consecutive bytes. The memory layout is RGB. + **/ + OrthancPluginPixelFormat_RGB24 = 4, + + /** + * @brief Color image in RGBA32 format. + * + * This format describes a color image. The pixels are stored in 4 + * consecutive bytes. The memory layout is RGBA. + **/ + OrthancPluginPixelFormat_RGBA32 = 5 + } OrthancPluginPixelFormat; + + + + /** + * The content types that are supported by Orthanc plugins. + **/ + typedef enum + { + OrthancPluginContentType_Unknown = 0, /*!< Unknown content type */ + OrthancPluginContentType_Dicom = 1, /*!< DICOM */ + OrthancPluginContentType_DicomAsJson = 2 /*!< JSON summary of a DICOM file */ + } OrthancPluginContentType; + + + + /** + * The supported type of DICOM resources. + **/ + typedef enum + { + OrthancPluginResourceType_Patient = 0, /*!< Patient */ + OrthancPluginResourceType_Study = 1, /*!< Study */ + OrthancPluginResourceType_Series = 2, /*!< Series */ + OrthancPluginResourceType_Instance = 3 /*!< Instance */ + } OrthancPluginResourceType; + + + + /** + * The supported type of changes that can happen to DICOM resources. + **/ + typedef enum + { + OrthancPluginChangeType_CompletedSeries = 0, /*!< Series is now complete */ + OrthancPluginChangeType_Deleted = 1, /*!< Deleted resource */ + OrthancPluginChangeType_NewChildInstance = 2, /*!< A new instance was added to this resource */ + OrthancPluginChangeType_NewInstance = 3, /*!< New instance received */ + OrthancPluginChangeType_NewPatient = 4, /*!< New patient created */ + OrthancPluginChangeType_NewSeries = 5, /*!< New series created */ + OrthancPluginChangeType_NewStudy = 6, /*!< New study created */ + OrthancPluginChangeType_StablePatient = 7, /*!< Timeout: No new instance in this patient */ + OrthancPluginChangeType_StableSeries = 8, /*!< Timeout: No new instance in this series */ + OrthancPluginChangeType_StableStudy = 9 /*!< Timeout: No new instance in this study */ + } OrthancPluginChangeType; + + + + /** + * @brief A memory buffer allocated by the core system of Orthanc. + * + * A memory buffer allocated by the core system of Orthanc. When the + * content of the buffer is not useful anymore, it must be free by a + * call to ::OrthancPluginFreeMemoryBuffer(). + **/ + typedef struct + { + /** + * @brief The content of the buffer. + **/ + void* data; + + /** + * @brief The number of bytes in the buffer. + **/ + uint32_t size; + } OrthancPluginMemoryBuffer; + + + + + /** + * @brief Opaque structure that represents the HTTP connection to the client application. + **/ + typedef struct _OrthancPluginRestOutput_t OrthancPluginRestOutput; + + + + /** + * @brief Opaque structure that represents a DICOM instance received by Orthanc. + **/ + typedef struct _OrthancPluginDicomInstance_t OrthancPluginDicomInstance; + + + + /** + * @brief Signature of a callback function that answers to a REST request. + **/ + typedef int32_t (*OrthancPluginRestCallback) ( + OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request); + + + + /** + * @brief Signature of a callback function that is triggered when Orthanc receives a DICOM instance. + **/ + typedef int32_t (*OrthancPluginOnStoredInstanceCallback) ( + OrthancPluginDicomInstance* instance, + const char* instanceId); + + + + /** + * @brief Signature of a callback function that is triggered when a change happens to some DICOM resource. + **/ + typedef int32_t (*OrthancPluginOnChangeCallback) ( + OrthancPluginChangeType changeType, + OrthancPluginResourceType resourceType, + const char* resourceId); + + + + /** + * @brief Signature of a function to free dynamic memory. + **/ + typedef void (*OrthancPluginFree) (void* buffer); + + + + /** + * @brief Callback for writing to the storage area. + * + * Signature of a callback function that is triggered when Orthanc writes a file to the storage area. + * + * @param uuid The UUID of the file. + * @param content The content of the file. + * @param size The size of the file. + * @param type The content type corresponding to this file. + * @return 0 if success, other value if error. + **/ + typedef int32_t (*OrthancPluginStorageCreate) ( + const char* uuid, + const void* content, + int64_t size, + OrthancPluginContentType type); + + + + /** + * @brief Callback for reading from the storage area. + * + * Signature of a callback function that is triggered when Orthanc reads a file from the storage area. + * + * @param content The content of the file (output). + * @param size The size of the file (output). + * @param uuid The UUID of the file of interest. + * @param type The content type corresponding to this file. + * @return 0 if success, other value if error. + **/ + typedef int32_t (*OrthancPluginStorageRead) ( + void** content, + int64_t* size, + const char* uuid, + OrthancPluginContentType type); + + + + /** + * @brief Callback for removing a file from the storage area. + * + * Signature of a callback function that is triggered when Orthanc deletes a file from the storage area. + * + * @param uuid The UUID of the file to be removed. + * @param type The content type corresponding to this file. + * @return 0 if success, other value if error. + **/ + typedef int32_t (*OrthancPluginStorageRemove) ( + const char* uuid, + OrthancPluginContentType type); + + + + /** + * @brief Data structure that contains information about the Orthanc core. + **/ + typedef struct _OrthancPluginContext_t + { + void* pluginsManager; + const char* orthancVersion; + OrthancPluginFree Free; + int32_t (*InvokeService) (struct _OrthancPluginContext_t* context, + _OrthancPluginService service, + const void* params); + } OrthancPluginContext; + + + + /** + * @brief Free a string. + * + * Free a string that was allocated by the core system of Orthanc. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param str The string to be freed. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginFreeString( + OrthancPluginContext* context, + char* str) + { + if (str != NULL) + { + context->Free(str); + } + } + + + /** + * @brief Check the compatibility of the plugin wrt. the version of its hosting Orthanc. + * + * This function checks whether the version of this C header is + * compatible with the current version of Orthanc. The result of + * this function should always be checked in the + * OrthancPluginInitialize() entry point of the plugin. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @return 1 if and only if the versions are compatible. If the + * result is 0, the initialization of the plugin should fail. + **/ + ORTHANC_PLUGIN_INLINE int OrthancPluginCheckVersion( + OrthancPluginContext* context) + { + int major, minor, revision; + + /* Assume compatibility with the mainline */ + if (!strcmp(context->orthancVersion, "mainline")) + { + return 1; + } + + /* Parse the version of the Orthanc core */ + if ( +#ifdef _MSC_VER + sscanf_s +#else + sscanf +#endif + (context->orthancVersion, "%4d.%4d.%4d", &major, &minor, &revision) != 3) + { + return 0; + } + + /* Check the major number of the version */ + + if (major > ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER) + { + return 1; + } + + if (major < ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER) + { + return 0; + } + + /* Check the minor number of the version */ + + if (minor > ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER) + { + return 1; + } + + if (minor < ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER) + { + return 0; + } + + /* Check the revision number of the version */ + + if (revision >= ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER) + { + return 1; + } + else + { + return 0; + } + } + + + /** + * @brief Free a memory buffer. + * + * Free a memory buffer that was allocated by the core system of Orthanc. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param buffer The memory buffer to release. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginFreeMemoryBuffer( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* buffer) + { + context->Free(buffer->data); + } + + + /** + * @brief Log an error. + * + * Log an error message using the Orthanc logging system. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param message The message to be logged. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginLogError( + OrthancPluginContext* context, + const char* message) + { + context->InvokeService(context, _OrthancPluginService_LogError, message); + } + + + /** + * @brief Log a warning. + * + * Log a warning message using the Orthanc logging system. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param message The message to be logged. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginLogWarning( + OrthancPluginContext* context, + const char* message) + { + context->InvokeService(context, _OrthancPluginService_LogWarning, message); + } + + + /** + * @brief Log an information. + * + * Log an information message using the Orthanc logging system. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param message The message to be logged. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginLogInfo( + OrthancPluginContext* context, + const char* message) + { + context->InvokeService(context, _OrthancPluginService_LogInfo, message); + } + + + + typedef struct + { + const char* pathRegularExpression; + OrthancPluginRestCallback callback; + } _OrthancPluginRestCallback; + + /** + * @brief Register a REST callback. + * + * This function registers a REST callback against a regular + * expression for a URI. This function must be called during the + * initialization of the plugin, i.e. inside the + * OrthancPluginInitialize() public function. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param pathRegularExpression Regular expression for the URI. May contain groups. + * @param callback The callback function to handle the REST call. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterRestCallback( + OrthancPluginContext* context, + const char* pathRegularExpression, + OrthancPluginRestCallback callback) + { + _OrthancPluginRestCallback params; + params.pathRegularExpression = pathRegularExpression; + params.callback = callback; + context->InvokeService(context, _OrthancPluginService_RegisterRestCallback, ¶ms); + } + + + + typedef struct + { + OrthancPluginOnStoredInstanceCallback callback; + } _OrthancPluginOnStoredInstanceCallback; + + /** + * @brief Register a callback for received instances. + * + * This function registers a callback function that is called + * whenever a new DICOM instance is stored into the Orthanc core. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param callback The callback function. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterOnStoredInstanceCallback( + OrthancPluginContext* context, + OrthancPluginOnStoredInstanceCallback callback) + { + _OrthancPluginOnStoredInstanceCallback params; + params.callback = callback; + + context->InvokeService(context, _OrthancPluginService_RegisterOnStoredInstanceCallback, ¶ms); + } + + + + typedef struct + { + OrthancPluginRestOutput* output; + const char* answer; + uint32_t answerSize; + const char* mimeType; + } _OrthancPluginAnswerBuffer; + + /** + * @brief Answer to a REST request. + * + * This function answers to a REST request with the content of a memory buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param output The HTTP connection to the client application. + * @param answer Pointer to the memory buffer containing the answer. + * @param answerSize Number of bytes of the answer. + * @param mimeType The MIME type of the answer. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginAnswerBuffer( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, + const char* answer, + uint32_t answerSize, + const char* mimeType) + { + _OrthancPluginAnswerBuffer params; + params.output = output; + params.answer = answer; + params.answerSize = answerSize; + params.mimeType = mimeType; + context->InvokeService(context, _OrthancPluginService_AnswerBuffer, ¶ms); + } + + + typedef struct + { + OrthancPluginRestOutput* output; + OrthancPluginPixelFormat format; + uint32_t width; + uint32_t height; + uint32_t pitch; + const void* buffer; + } _OrthancPluginCompressAndAnswerPngImage; + + /** + * @brief Answer to a REST request with a PNG image. + * + * This function answers to a REST request with a PNG image. The + * parameters of this function describe a memory buffer that + * contains an uncompressed image. The image will be automatically compressed + * as a PNG image by the core system of Orthanc. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param output The HTTP connection to the client application. + * @param format The memory layout of the uncompressed image. + * @param width The width of the image. + * @param height The height of the image. + * @param pitch The pitch of the image (i.e. the number of bytes + * between 2 successive lines of the image in the memory buffer. + * @param buffer The memory buffer containing the uncompressed image. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginCompressAndAnswerPngImage( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, + OrthancPluginPixelFormat format, + uint32_t width, + uint32_t height, + uint32_t pitch, + const void* buffer) + { + _OrthancPluginCompressAndAnswerPngImage params; + params.output = output; + params.format = format; + params.width = width; + params.height = height; + params.pitch = pitch; + params.buffer = buffer; + context->InvokeService(context, _OrthancPluginService_CompressAndAnswerPngImage, ¶ms); + } + + + + typedef struct + { + OrthancPluginMemoryBuffer* target; + const char* instanceId; + } _OrthancPluginGetDicomForInstance; + + /** + * @brief Retrieve a DICOM instance using its Orthanc identifier. + * + * Retrieve a DICOM instance using its Orthanc identifier. The DICOM + * file is stored into a newly allocated memory buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. + * @param instanceId The Orthanc identifier of the DICOM instance of interest. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE int OrthancPluginGetDicomForInstance( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const char* instanceId) + { + _OrthancPluginGetDicomForInstance params; + params.target = target; + params.instanceId = instanceId; + return context->InvokeService(context, _OrthancPluginService_GetDicomForInstance, ¶ms); + } + + + + typedef struct + { + OrthancPluginMemoryBuffer* target; + const char* uri; + } _OrthancPluginRestApiGet; + + /** + * @brief Make a GET call to the built-in Orthanc REST API. + * + * Make a GET call to the built-in Orthanc REST API. The result to + * the query is stored into a newly allocated memory buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. + * @param uri The URI in the built-in Orthanc API. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE int OrthancPluginRestApiGet( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const char* uri) + { + _OrthancPluginRestApiGet params; + params.target = target; + params.uri = uri; + return context->InvokeService(context, _OrthancPluginService_RestApiGet, ¶ms); + } + + + + /** + * @brief Make a GET call to the REST API, as tainted by the plugins. + * + * Make a GET call to the Orthanc REST API, after all the plugins + * are applied. In other words, if some plugin overrides or adds the + * called URI to the built-in Orthanc REST API, this call will + * return the result provided by this plugin. The result to the + * query is stored into a newly allocated memory buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. + * @param uri The URI in the built-in Orthanc API. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE int OrthancPluginRestApiGetAfterPlugins( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const char* uri) + { + _OrthancPluginRestApiGet params; + params.target = target; + params.uri = uri; + return context->InvokeService(context, _OrthancPluginService_RestApiGetAfterPlugins, ¶ms); + } + + + + typedef struct + { + OrthancPluginMemoryBuffer* target; + const char* uri; + const char* body; + uint32_t bodySize; + } _OrthancPluginRestApiPostPut; + + /** + * @brief Make a POST call to the built-in Orthanc REST API. + * + * Make a POST call to the built-in Orthanc REST API. The result to + * the query is stored into a newly allocated memory buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. + * @param uri The URI in the built-in Orthanc API. + * @param body The body of the POST request. + * @param bodySize The size of the body. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE int OrthancPluginRestApiPost( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const char* uri, + const char* body, + uint32_t bodySize) + { + _OrthancPluginRestApiPostPut params; + params.target = target; + params.uri = uri; + params.body = body; + params.bodySize = bodySize; + return context->InvokeService(context, _OrthancPluginService_RestApiPost, ¶ms); + } + + + /** + * @brief Make a POST call to the REST API, as tainted by the plugins. + * + * Make a POST call to the Orthanc REST API, after all the plugins + * are applied. In other words, if some plugin overrides or adds the + * called URI to the built-in Orthanc REST API, this call will + * return the result provided by this plugin. The result to the + * query is stored into a newly allocated memory buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. + * @param uri The URI in the built-in Orthanc API. + * @param body The body of the POST request. + * @param bodySize The size of the body. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE int OrthancPluginRestApiPostAfterPlugins( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const char* uri, + const char* body, + uint32_t bodySize) + { + _OrthancPluginRestApiPostPut params; + params.target = target; + params.uri = uri; + params.body = body; + params.bodySize = bodySize; + return context->InvokeService(context, _OrthancPluginService_RestApiPostAfterPlugins, ¶ms); + } + + + + /** + * @brief Make a DELETE call to the built-in Orthanc REST API. + * + * Make a DELETE call to the built-in Orthanc REST API. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param uri The URI to delete in the built-in Orthanc API. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE int OrthancPluginRestApiDelete( + OrthancPluginContext* context, + const char* uri) + { + return context->InvokeService(context, _OrthancPluginService_RestApiDelete, uri); + } + + + /** + * @brief Make a DELETE call to the REST API, as tainted by the plugins. + * + * Make a DELETE call to the Orthanc REST API, after all the plugins + * are applied. In other words, if some plugin overrides or adds the + * called URI to the built-in Orthanc REST API, this call will + * return the result provided by this plugin. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param uri The URI to delete in the built-in Orthanc API. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE int OrthancPluginRestApiDeleteAfterPlugins( + OrthancPluginContext* context, + const char* uri) + { + return context->InvokeService(context, _OrthancPluginService_RestApiDeleteAfterPlugins, uri); + } + + + + /** + * @brief Make a PUT call to the built-in Orthanc REST API. + * + * Make a PUT call to the built-in Orthanc REST API. The result to + * the query is stored into a newly allocated memory buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. + * @param uri The URI in the built-in Orthanc API. + * @param body The body of the PUT request. + * @param bodySize The size of the body. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE int OrthancPluginRestApiPut( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const char* uri, + const char* body, + uint32_t bodySize) + { + _OrthancPluginRestApiPostPut params; + params.target = target; + params.uri = uri; + params.body = body; + params.bodySize = bodySize; + return context->InvokeService(context, _OrthancPluginService_RestApiPut, ¶ms); + } + + + + /** + * @brief Make a PUT call to the REST API, as tainted by the plugins. + * + * Make a PUT call to the Orthanc REST API, after all the plugins + * are applied. In other words, if some plugin overrides or adds the + * called URI to the built-in Orthanc REST API, this call will + * return the result provided by this plugin. The result to the + * query is stored into a newly allocated memory buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. + * @param uri The URI in the built-in Orthanc API. + * @param body The body of the PUT request. + * @param bodySize The size of the body. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_INLINE int OrthancPluginRestApiPutAfterPlugins( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const char* uri, + const char* body, + uint32_t bodySize) + { + _OrthancPluginRestApiPostPut params; + params.target = target; + params.uri = uri; + params.body = body; + params.bodySize = bodySize; + return context->InvokeService(context, _OrthancPluginService_RestApiPutAfterPlugins, ¶ms); + } + + + + typedef struct + { + OrthancPluginRestOutput* output; + const char* argument; + } _OrthancPluginOutputPlusArgument; + + /** + * @brief Redirect a REST request. + * + * This function answers to a REST request by redirecting the user + * to another URI using HTTP status 301. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param output The HTTP connection to the client application. + * @param redirection Where to redirect. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginRedirect( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, + const char* redirection) + { + _OrthancPluginOutputPlusArgument params; + params.output = output; + params.argument = redirection; + context->InvokeService(context, _OrthancPluginService_Redirect, ¶ms); + } + + + + typedef struct + { + char** result; + const char* argument; + } _OrthancPluginRetrieveDynamicString; + + /** + * @brief Look for a patient. + * + * Look for a patient stored in Orthanc, using its Patient ID tag (0x0010, 0x0020). + * This function uses the database index to run as fast as possible (it does not loop + * over all the stored patients). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param patientID The Patient ID of interest. + * @return The NULL value if the patient is non-existent, or a string containing the + * Orthanc ID of the patient. This string must be freed by OrthancPluginFreeString(). + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupPatient( + OrthancPluginContext* context, + const char* patientID) + { + char* result; + + _OrthancPluginRetrieveDynamicString params; + params.result = &result; + params.argument = patientID; + + if (context->InvokeService(context, _OrthancPluginService_LookupPatient, ¶ms)) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Look for a study. + * + * Look for a study stored in Orthanc, using its Study Instance UID tag (0x0020, 0x000d). + * This function uses the database index to run as fast as possible (it does not loop + * over all the stored studies). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param studyUID The Study Instance UID of interest. + * @return The NULL value if the study is non-existent, or a string containing the + * Orthanc ID of the study. This string must be freed by OrthancPluginFreeString(). + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupStudy( + OrthancPluginContext* context, + const char* studyUID) + { + char* result; + + _OrthancPluginRetrieveDynamicString params; + params.result = &result; + params.argument = studyUID; + + if (context->InvokeService(context, _OrthancPluginService_LookupStudy, ¶ms)) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Look for a study, using the accession number. + * + * Look for a study stored in Orthanc, using its Accession Number tag (0x0008, 0x0050). + * This function uses the database index to run as fast as possible (it does not loop + * over all the stored studies). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param accessionNumber The Accession Number of interest. + * @return The NULL value if the study is non-existent, or a string containing the + * Orthanc ID of the study. This string must be freed by OrthancPluginFreeString(). + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupStudyWithAccessionNumber( + OrthancPluginContext* context, + const char* accessionNumber) + { + char* result; + + _OrthancPluginRetrieveDynamicString params; + params.result = &result; + params.argument = accessionNumber; + + if (context->InvokeService(context, _OrthancPluginService_LookupStudyWithAccessionNumber, ¶ms)) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Look for a series. + * + * Look for a series stored in Orthanc, using its Series Instance UID tag (0x0020, 0x000e). + * This function uses the database index to run as fast as possible (it does not loop + * over all the stored series). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param seriesUID The Series Instance UID of interest. + * @return The NULL value if the series is non-existent, or a string containing the + * Orthanc ID of the series. This string must be freed by OrthancPluginFreeString(). + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupSeries( + OrthancPluginContext* context, + const char* seriesUID) + { + char* result; + + _OrthancPluginRetrieveDynamicString params; + params.result = &result; + params.argument = seriesUID; + + if (context->InvokeService(context, _OrthancPluginService_LookupSeries, ¶ms)) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Look for an instance. + * + * Look for an instance stored in Orthanc, using its SOP Instance UID tag (0x0008, 0x0018). + * This function uses the database index to run as fast as possible (it does not loop + * over all the stored instances). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param sopInstanceUID The SOP Instance UID of interest. + * @return The NULL value if the instance is non-existent, or a string containing the + * Orthanc ID of the instance. This string must be freed by OrthancPluginFreeString(). + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupInstance( + OrthancPluginContext* context, + const char* sopInstanceUID) + { + char* result; + + _OrthancPluginRetrieveDynamicString params; + params.result = &result; + params.argument = sopInstanceUID; + + if (context->InvokeService(context, _OrthancPluginService_LookupInstance, ¶ms)) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + + typedef struct + { + OrthancPluginRestOutput* output; + uint16_t status; + } _OrthancPluginSendHttpStatusCode; + + /** + * @brief Send a HTTP status code. + * + * This function answers to a REST request by sending a HTTP status + * code (such as "400 - Bad Request"). Note that: + * - Successful requests (status 200) must use ::OrthancPluginAnswerBuffer(). + * - Redirections (status 301) must use ::OrthancPluginRedirect(). + * - Unauthorized access (status 401) must use ::OrthancPluginSendUnauthorized(). + * - Methods not allowed (status 405) must use ::OrthancPluginSendMethodNotAllowed(). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param output The HTTP connection to the client application. + * @param status The HTTP status code to be sent. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginSendHttpStatusCode( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, + uint16_t status) + { + _OrthancPluginSendHttpStatusCode params; + params.output = output; + params.status = status; + context->InvokeService(context, _OrthancPluginService_SendHttpStatusCode, ¶ms); + } + + + /** + * @brief Signal that a REST request is not authorized. + * + * This function answers to a REST request by signaling that it is + * not authorized. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param output The HTTP connection to the client application. + * @param realm The realm for the authorization process. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginSendUnauthorized( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, + const char* realm) + { + _OrthancPluginOutputPlusArgument params; + params.output = output; + params.argument = realm; + context->InvokeService(context, _OrthancPluginService_SendUnauthorized, ¶ms); + } + + + /** + * @brief Signal that this URI does not support this HTTP method. + * + * This function answers to a REST request by signaling that the + * queried URI does not support this method. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param output The HTTP connection to the client application. + * @param allowedMethods The allowed methods for this URI (e.g. "GET,POST" after a PUT or a POST request). + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginSendMethodNotAllowed( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, + const char* allowedMethods) + { + _OrthancPluginOutputPlusArgument params; + params.output = output; + params.argument = allowedMethods; + context->InvokeService(context, _OrthancPluginService_SendMethodNotAllowed, ¶ms); + } + + + typedef struct + { + OrthancPluginRestOutput* output; + const char* key; + const char* value; + } _OrthancPluginSetHttpHeader; + + /** + * @brief Set a cookie. + * + * This function sets a cookie in the HTTP client. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param output The HTTP connection to the client application. + * @param cookie The cookie to be set. + * @param value The value of the cookie. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginSetCookie( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, + const char* cookie, + const char* value) + { + _OrthancPluginSetHttpHeader params; + params.output = output; + params.key = cookie; + params.value = value; + context->InvokeService(context, _OrthancPluginService_SetCookie, ¶ms); + } + + + /** + * @brief Set some HTTP header. + * + * This function sets a HTTP header in the HTTP answer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param output The HTTP connection to the client application. + * @param key The HTTP header to be set. + * @param value The value of the HTTP header. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginSetHttpHeader( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, + const char* key, + const char* value) + { + _OrthancPluginSetHttpHeader params; + params.output = output; + params.key = key; + params.value = value; + context->InvokeService(context, _OrthancPluginService_SetHttpHeader, ¶ms); + } + + + typedef struct + { + char** resultStringToFree; + const char** resultString; + int64_t* resultInt64; + const char* key; + OrthancPluginDicomInstance* instance; + } _OrthancPluginAccessDicomInstance; + + + /** + * @brief Get the AET of a DICOM instance. + * + * This function returns the Application Entity Title (AET) of the + * DICOM modality from which a DICOM instance originates. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The instance of interest. + * @return The AET if success, NULL if error. + **/ + ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetInstanceRemoteAet( + OrthancPluginContext* context, + OrthancPluginDicomInstance* instance) + { + const char* result; + + _OrthancPluginAccessDicomInstance params; + memset(¶ms, 0, sizeof(params)); + params.resultString = &result; + params.instance = instance; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceRemoteAet, ¶ms)) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Get the size of a DICOM file. + * + * This function returns the number of bytes of the given DICOM instance. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The instance of interest. + * @return The size of the file, -1 in case of error. + **/ + ORTHANC_PLUGIN_INLINE int64_t OrthancPluginGetInstanceSize( + OrthancPluginContext* context, + OrthancPluginDicomInstance* instance) + { + int64_t size; + + _OrthancPluginAccessDicomInstance params; + memset(¶ms, 0, sizeof(params)); + params.resultInt64 = &size; + params.instance = instance; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceSize, ¶ms)) + { + /* Error */ + return -1; + } + else + { + return size; + } + } + + + /** + * @brief Get the data of a DICOM file. + * + * This function returns a pointer to the content of the given DICOM instance. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The instance of interest. + * @return The pointer to the DICOM data, NULL in case of error. + **/ + ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetInstanceData( + OrthancPluginContext* context, + OrthancPluginDicomInstance* instance) + { + const char* result; + + _OrthancPluginAccessDicomInstance params; + memset(¶ms, 0, sizeof(params)); + params.resultString = &result; + params.instance = instance; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceData, ¶ms)) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Get the DICOM tag hierarchy as a JSON file. + * + * This function returns a pointer to a newly created string + * containing a JSON file. This JSON file encodes the tag hierarchy + * of the given DICOM instance. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The instance of interest. + * @return The NULL value in case of error, or a string containing the JSON file. + * This string must be freed by OrthancPluginFreeString(). + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceJson( + OrthancPluginContext* context, + OrthancPluginDicomInstance* instance) + { + char* result; + + _OrthancPluginAccessDicomInstance params; + memset(¶ms, 0, sizeof(params)); + params.resultStringToFree = &result; + params.instance = instance; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceJson, ¶ms)) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Get the DICOM tag hierarchy as a JSON file (with simplification). + * + * This function returns a pointer to a newly created string + * containing a JSON file. This JSON file encodes the tag hierarchy + * of the given DICOM instance. In contrast with + * ::OrthancPluginGetInstanceJson(), the returned JSON file is in + * its simplified version. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The instance of interest. + * @return The NULL value in case of error, or a string containing the JSON file. + * This string must be freed by OrthancPluginFreeString(). + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceSimplifiedJson( + OrthancPluginContext* context, + OrthancPluginDicomInstance* instance) + { + char* result; + + _OrthancPluginAccessDicomInstance params; + memset(¶ms, 0, sizeof(params)); + params.resultStringToFree = &result; + params.instance = instance; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceSimplifiedJson, ¶ms)) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Check whether a DICOM instance is associated with some metadata. + * + * This function checks whether the DICOM instance of interest is + * associated with some metadata. As of Orthanc 0.8.1, in the + * callbacks registered by + * ::OrthancPluginRegisterOnStoredInstanceCallback(), the only + * possibly available metadata are "ReceptionDate", "RemoteAET" and + * "IndexInSeries". + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The instance of interest. + * @param metadata The metadata of interest. + * @return 1 if the metadata is present, 0 if it is absent, -1 in case of error. + **/ + ORTHANC_PLUGIN_INLINE int OrthancPluginHasInstanceMetadata( + OrthancPluginContext* context, + OrthancPluginDicomInstance* instance, + const char* metadata) + { + int64_t result; + + _OrthancPluginAccessDicomInstance params; + memset(¶ms, 0, sizeof(params)); + params.resultInt64 = &result; + params.instance = instance; + params.key = metadata; + + if (context->InvokeService(context, _OrthancPluginService_HasInstanceMetadata, ¶ms)) + { + /* Error */ + return -1; + } + else + { + return (result != 0); + } + } + + + /** + * @brief Get the value of some metadata associated with a given DICOM instance. + * + * This functions returns the value of some metadata that is associated with the DICOM instance of interest. + * Before calling this function, the existence of the metadata must have been checked with + * ::OrthancPluginHasInstanceMetadata(). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The instance of interest. + * @param metadata The metadata of interest. + * @return The metadata value if success, NULL if error. + **/ + ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetInstanceMetadata( + OrthancPluginContext* context, + OrthancPluginDicomInstance* instance, + const char* metadata) + { + const char* result; + + _OrthancPluginAccessDicomInstance params; + memset(¶ms, 0, sizeof(params)); + params.resultString = &result; + params.instance = instance; + params.key = metadata; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceMetadata, ¶ms)) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + + typedef struct + { + OrthancPluginStorageCreate create; + OrthancPluginStorageRead read; + OrthancPluginStorageRemove remove; + OrthancPluginFree free; + } _OrthancPluginRegisterStorageArea; + + /** + * @brief Register a custom storage area. + * + * This function registers a custom storage area, to replace the + * built-in way Orthanc stores its files on the filesystem. This + * function must be called during the initialization of the plugin, + * i.e. inside the OrthancPluginInitialize() public function. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param create The callback function to store a file on the custom storage area. + * @param read The callback function to read a file from the custom storage area. + * @param remove The callback function to remove a file from the custom storage area. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea( + OrthancPluginContext* context, + OrthancPluginStorageCreate create, + OrthancPluginStorageRead read, + OrthancPluginStorageRemove remove) + { + _OrthancPluginRegisterStorageArea params; + params.create = create; + params.read = read; + params.remove = remove; + +#ifdef __cplusplus + params.free = ::free; +#else + params.free = free; +#endif + + context->InvokeService(context, _OrthancPluginService_RegisterStorageArea, ¶ms); + } + + + + /** + * @brief Return the path to the Orthanc executable. + * + * This function returns the path to the Orthanc executable. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @return NULL in the case of an error, or a newly allocated string + * containing the path. This string must be freed by + * OrthancPluginFreeString(). + **/ + ORTHANC_PLUGIN_INLINE char *OrthancPluginGetOrthancPath(OrthancPluginContext* context) + { + char* result; + + _OrthancPluginRetrieveDynamicString params; + params.result = &result; + params.argument = NULL; + + if (context->InvokeService(context, _OrthancPluginService_GetOrthancPath, ¶ms)) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Return the directory containing the Orthanc. + * + * This function returns the path to the directory containing the Orthanc executable. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @return NULL in the case of an error, or a newly allocated string + * containing the path. This string must be freed by + * OrthancPluginFreeString(). + **/ + ORTHANC_PLUGIN_INLINE char *OrthancPluginGetOrthancDirectory(OrthancPluginContext* context) + { + char* result; + + _OrthancPluginRetrieveDynamicString params; + params.result = &result; + params.argument = NULL; + + if (context->InvokeService(context, _OrthancPluginService_GetOrthancDirectory, ¶ms)) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Return the path to the configuration file. + * + * This function returns the path to the configuration file that was + * specified when starting Orthanc. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @return NULL in the case of an error, or a newly allocated string + * containing the path. This string must be freed by + * OrthancPluginFreeString(). + **/ + ORTHANC_PLUGIN_INLINE char *OrthancPluginGetConfigurationPath(OrthancPluginContext* context) + { + char* result; + + _OrthancPluginRetrieveDynamicString params; + params.result = &result; + params.argument = NULL; + + if (context->InvokeService(context, _OrthancPluginService_GetConfigurationPath, ¶ms)) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + + typedef struct + { + OrthancPluginOnChangeCallback callback; + } _OrthancPluginOnChangeCallback; + + /** + * @brief Register a callback to monitor changes. + * + * This function registers a callback function that is called + * whenever a change happens to some DICOM resource. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param callback The callback function. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterOnChangeCallback( + OrthancPluginContext* context, + OrthancPluginOnChangeCallback callback) + { + _OrthancPluginOnChangeCallback params; + params.callback = callback; + + context->InvokeService(context, _OrthancPluginService_RegisterOnChangeCallback, ¶ms); + } + + + + typedef struct + { + const char* plugin; + _OrthancPluginProperty property; + const char* value; + } _OrthancPluginSetPluginProperty; + + + /** + * @brief Set the URI where the plugin provides its Web interface. + * + * For plugins that come with a Web interface, this function + * declares the entry path where to find this interface. This + * information is notably used in the "Plugins" page of Orthanc + * Explorer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param uri The root URI for this plugin. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginSetRootUri( + OrthancPluginContext* context, + const char* uri) + { + _OrthancPluginSetPluginProperty params; + params.plugin = OrthancPluginGetName(); + params.property = _OrthancPluginProperty_RootUri; + params.value = uri; + + context->InvokeService(context, _OrthancPluginService_SetPluginProperty, ¶ms); + } + + + /** + * @brief Set a description for this plugin. + * + * Set a description for this plugin. It is displayed in the + * "Plugins" page of Orthanc Explorer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param description The description. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginSetDescription( + OrthancPluginContext* context, + const char* description) + { + _OrthancPluginSetPluginProperty params; + params.plugin = OrthancPluginGetName(); + params.property = _OrthancPluginProperty_Description; + params.value = description; + + context->InvokeService(context, _OrthancPluginService_SetPluginProperty, ¶ms); + } + + + /** + * @brief Extend the JavaScript code of Orthanc Explorer. + * + * Add JavaScript code to customize the default behavior of Orthanc + * Explorer. This can for instance be used to add new buttons. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param javascript The custom JavaScript code. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginExtendOrthancExplorer( + OrthancPluginContext* context, + const char* javascript) + { + _OrthancPluginSetPluginProperty params; + params.plugin = OrthancPluginGetName(); + params.property = _OrthancPluginProperty_OrthancExplorer; + params.value = javascript; + + context->InvokeService(context, _OrthancPluginService_SetPluginProperty, ¶ms); + } + + + typedef struct + { + char** result; + int32_t property; + const char* value; + } _OrthancPluginGlobalProperty; + + + /** + * @brief Get the value of a global property. + * + * Get the value of a global property that is stored in the Orthanc database. Global + * properties whose index is below 1024 are reserved by Orthanc. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param property The global property of interest. + * @param defaultValue The value to return, if the global property is unset. + * @return The value of the global property, or NULL in the case of an error. This + * string must be freed by OrthancPluginFreeString(). + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginGetGlobalProperty( + OrthancPluginContext* context, + int32_t property, + const char* defaultValue) + { + char* result; + + _OrthancPluginGlobalProperty params; + params.result = &result; + params.property = property; + params.value = defaultValue; + + if (context->InvokeService(context, _OrthancPluginService_GetGlobalProperty, ¶ms)) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Set the value of a global property. + * + * Set the value of a global property into the Orthanc + * database. Setting a global property can be used by plugins to + * save their internal parameters. Plugins are only allowed to set + * properties whose index are above or equal to 1024 (properties + * below 1024 are read-only and reserved by Orthanc). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param property The global property of interest. + * @param value The value to be set in the global property. + * @return 0 if success, -1 in case of error. + **/ + ORTHANC_PLUGIN_INLINE int32_t OrthancPluginSetGlobalProperty( + OrthancPluginContext* context, + int32_t property, + const char* value) + { + _OrthancPluginGlobalProperty params; + params.result = NULL; + params.property = property; + params.value = value; + + if (context->InvokeService(context, _OrthancPluginService_SetGlobalProperty, ¶ms)) + { + /* Error */ + return -1; + } + else + { + return 0; + } + } + + + + typedef struct + { + int32_t *resultInt32; + uint32_t *resultUint32; + int64_t *resultInt64; + uint64_t *resultUint64; + } _OrthancPluginReturnSingleValue; + + /** + * @brief Get the number of command-line arguments. + * + * Retrieve the number of command-line arguments that were used to launch Orthanc. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @return The number of arguments. + **/ + ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetCommandLineArgumentsCount( + OrthancPluginContext* context) + { + uint32_t count = 0; + + _OrthancPluginReturnSingleValue params; + memset(¶ms, 0, sizeof(params)); + params.resultUint32 = &count; + + if (context->InvokeService(context, _OrthancPluginService_GetCommandLineArgumentsCount, ¶ms)) + { + /* Error */ + return 0; + } + else + { + return count; + } + } + + + + /** + * @brief Get the value of a command-line argument. + * + * Get the value of one of the command-line arguments that were used + * to launch Orthanc. The number of available arguments can be + * retrieved by OrthancPluginGetCommandLineArgumentsCount(). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param argument The index of the argument. + * @return The value of the argument, or NULL in the case of an error. This + * string must be freed by OrthancPluginFreeString(). + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginGetCommandLineArgument( + OrthancPluginContext* context, + uint32_t argument) + { + char* result; + + _OrthancPluginGlobalProperty params; + params.result = &result; + params.property = (int32_t) argument; + params.value = NULL; + + if (context->InvokeService(context, _OrthancPluginService_GetCommandLineArgument, ¶ms)) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + +#ifdef __cplusplus +} +#endif + + +/** @} */ +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Include/OrthancCppDatabasePlugin.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,1532 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "OrthancCDatabasePlugin.h" + +#include <stdexcept> +#include <list> +#include <string> + +namespace OrthancPlugins +{ + // This class mimics "boost::noncopyable" + class NonCopyable + { + private: + NonCopyable(const NonCopyable&); + + NonCopyable& operator= (const NonCopyable&); + + protected: + NonCopyable() + { + } + + ~NonCopyable() + { + } + }; + + + + class DatabaseBackendOutput : public NonCopyable + { + friend class DatabaseBackendAdapter; + + private: + enum AllowedAnswers + { + AllowedAnswers_All, + AllowedAnswers_None, + AllowedAnswers_Attachment, + AllowedAnswers_Change, + AllowedAnswers_DicomTag, + AllowedAnswers_ExportedResource + }; + + OrthancPluginContext* context_; + OrthancPluginDatabaseContext* database_; + AllowedAnswers allowedAnswers_; + + void SetAllowedAnswers(AllowedAnswers allowed) + { + allowedAnswers_ = allowed; + } + + public: + DatabaseBackendOutput(OrthancPluginContext* context, + OrthancPluginDatabaseContext* database) : + context_(context), + database_(database), + allowedAnswers_(AllowedAnswers_All /* for unit tests */) + { + } + + void LogError(const std::string& message) + { + OrthancPluginLogError(context_, message.c_str()); + } + + void LogWarning(const std::string& message) + { + OrthancPluginLogWarning(context_, message.c_str()); + } + + void LogInfo(const std::string& message) + { + OrthancPluginLogInfo(context_, message.c_str()); + } + + void SignalDeletedAttachment(const std::string& uuid, + int32_t contentType, + uint64_t uncompressedSize, + const std::string& uncompressedHash, + int32_t compressionType, + uint64_t compressedSize, + const std::string& compressedHash) + { + OrthancPluginAttachment attachment; + attachment.uuid = uuid.c_str(); + attachment.contentType = contentType; + attachment.uncompressedSize = uncompressedSize; + attachment.uncompressedHash = uncompressedHash.c_str(); + attachment.compressionType = compressionType; + attachment.compressedSize = compressedSize; + attachment.compressedHash = compressedHash.c_str(); + + OrthancPluginDatabaseSignalDeletedAttachment(context_, database_, &attachment); + } + + void SignalDeletedResource(const std::string& publicId, + OrthancPluginResourceType resourceType) + { + OrthancPluginDatabaseSignalDeletedResource(context_, database_, publicId.c_str(), resourceType); + } + + void SignalRemainingAncestor(const std::string& ancestorId, + OrthancPluginResourceType ancestorType) + { + OrthancPluginDatabaseSignalRemainingAncestor(context_, database_, ancestorId.c_str(), ancestorType); + } + + void AnswerAttachment(const std::string& uuid, + int32_t contentType, + uint64_t uncompressedSize, + const std::string& uncompressedHash, + int32_t compressionType, + uint64_t compressedSize, + const std::string& compressedHash) + { + if (allowedAnswers_ != AllowedAnswers_All && + allowedAnswers_ != AllowedAnswers_Attachment) + { + throw std::runtime_error("Cannot answer with an attachment in the current state"); + } + + OrthancPluginAttachment attachment; + attachment.uuid = uuid.c_str(); + attachment.contentType = contentType; + attachment.uncompressedSize = uncompressedSize; + attachment.uncompressedHash = uncompressedHash.c_str(); + attachment.compressionType = compressionType; + attachment.compressedSize = compressedSize; + attachment.compressedHash = compressedHash.c_str(); + + OrthancPluginDatabaseAnswerAttachment(context_, database_, &attachment); + } + + void AnswerChange(int64_t seq, + int32_t changeType, + OrthancPluginResourceType resourceType, + const std::string& publicId, + const std::string& date) + { + if (allowedAnswers_ != AllowedAnswers_All && + allowedAnswers_ != AllowedAnswers_Change) + { + throw std::runtime_error("Cannot answer with a change in the current state"); + } + + OrthancPluginChange change; + change.seq = seq; + change.changeType = changeType; + change.resourceType = resourceType; + change.publicId = publicId.c_str(); + change.date = date.c_str(); + + OrthancPluginDatabaseAnswerChange(context_, database_, &change); + } + + void AnswerDicomTag(uint16_t group, + uint16_t element, + const std::string& value) + { + if (allowedAnswers_ != AllowedAnswers_All && + allowedAnswers_ != AllowedAnswers_DicomTag) + { + throw std::runtime_error("Cannot answer with a DICOM tag in the current state"); + } + + OrthancPluginDicomTag tag; + tag.group = group; + tag.element = element; + tag.value = value.c_str(); + + OrthancPluginDatabaseAnswerDicomTag(context_, database_, &tag); + } + + void AnswerExportedResource(int64_t seq, + OrthancPluginResourceType resourceType, + const std::string& publicId, + const std::string& modality, + const std::string& date, + const std::string& patientId, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + const std::string& sopInstanceUid) + { + if (allowedAnswers_ != AllowedAnswers_All && + allowedAnswers_ != AllowedAnswers_ExportedResource) + { + throw std::runtime_error("Cannot answer with an exported resource in the current state"); + } + + OrthancPluginExportedResource exported; + exported.seq = seq; + exported.resourceType = resourceType; + exported.publicId = publicId.c_str(); + exported.modality = modality.c_str(); + exported.date = date.c_str(); + exported.patientId = patientId.c_str(); + exported.studyInstanceUid = studyInstanceUid.c_str(); + exported.seriesInstanceUid = seriesInstanceUid.c_str(); + exported.sopInstanceUid = sopInstanceUid.c_str(); + + OrthancPluginDatabaseAnswerExportedResource(context_, database_, &exported); + } + }; + + + + class IDatabaseBackend : public NonCopyable + { + friend class DatabaseBackendAdapter; + + private: + DatabaseBackendOutput* output_; + + void Finalize() + { + if (output_ != NULL) + { + delete output_; + output_ = NULL; + } + } + + protected: + DatabaseBackendOutput& GetOutput() + { + return *output_; + } + + public: + IDatabaseBackend() : output_(NULL) + { + } + + virtual ~IDatabaseBackend() + { + Finalize(); + } + + // This takes the ownership + void RegisterOutput(DatabaseBackendOutput* output) + { + Finalize(); + output_ = output; + } + + virtual void Open() = 0; + + virtual void Close() = 0; + + virtual void AddAttachment(int64_t id, + const OrthancPluginAttachment& attachment) = 0; + + virtual void AttachChild(int64_t parent, + int64_t child) = 0; + + virtual void ClearChanges() = 0; + + virtual void ClearExportedResources() = 0; + + virtual int64_t CreateResource(const char* publicId, + OrthancPluginResourceType type) = 0; + + virtual void DeleteAttachment(int64_t id, + int32_t attachment) = 0; + + virtual void DeleteMetadata(int64_t id, + int32_t metadataType) = 0; + + virtual void DeleteResource(int64_t id) = 0; + + virtual void GetAllPublicIds(std::list<std::string>& target, + OrthancPluginResourceType resourceType) = 0; + + /* Use GetOutput().AnswerChange() */ + virtual void GetChanges(bool& done /*out*/, + int64_t since, + uint32_t maxResults) = 0; + + virtual void GetChildrenInternalId(std::list<int64_t>& target /*out*/, + int64_t id) = 0; + + virtual void GetChildrenPublicId(std::list<std::string>& target /*out*/, + int64_t id) = 0; + + /* Use GetOutput().AnswerExportedResource() */ + virtual void GetExportedResources(bool& done /*out*/, + int64_t since, + uint32_t maxResults) = 0; + + /* Use GetOutput().AnswerChange() */ + virtual void GetLastChange() = 0; + + /* Use GetOutput().AnswerExportedResource() */ + virtual void GetLastExportedResource() = 0; + + /* Use GetOutput().AnswerDicomTag() */ + virtual void GetMainDicomTags(int64_t id) = 0; + + virtual std::string GetPublicId(int64_t resourceId) = 0; + + virtual uint64_t GetResourceCount(OrthancPluginResourceType resourceType) = 0; + + virtual OrthancPluginResourceType GetResourceType(int64_t resourceId) = 0; + + virtual uint64_t GetTotalCompressedSize() = 0; + + virtual uint64_t GetTotalUncompressedSize() = 0; + + virtual bool IsExistingResource(int64_t internalId) = 0; + + virtual bool IsProtectedPatient(int64_t internalId) = 0; + + virtual void ListAvailableMetadata(std::list<int32_t>& target /*out*/, + int64_t id) = 0; + + virtual void ListAvailableAttachments(std::list<int32_t>& target /*out*/, + int64_t id) = 0; + + virtual void LogChange(const OrthancPluginChange& change) = 0; + + virtual void LogExportedResource(const OrthancPluginExportedResource& resource) = 0; + + /* Use GetOutput().AnswerAttachment() */ + virtual bool LookupAttachment(int64_t id, + int32_t contentType) = 0; + + virtual bool LookupGlobalProperty(std::string& target /*out*/, + int32_t property) = 0; + + /** + * "Identifiers" are necessarily one of the following tags: + * PatientID (0x0010, 0x0020), StudyInstanceUID (0x0020, 0x000d), + * SeriesInstanceUID (0x0020, 0x000e), SOPInstanceUID (0x0008, + * 0x0018) or AccessionNumber (0x0008, 0x0050). + **/ + virtual void LookupIdentifier(std::list<int64_t>& target /*out*/, + uint16_t group, + uint16_t element, + const char* value) = 0; + + virtual void LookupIdentifier(std::list<int64_t>& target /*out*/, + const char* value) = 0; + + virtual bool LookupMetadata(std::string& target /*out*/, + int64_t id, + int32_t metadataType) = 0; + + virtual bool LookupParent(int64_t& parentId /*out*/, + int64_t resourceId) = 0; + + virtual bool LookupResource(int64_t& id /*out*/, + OrthancPluginResourceType& type /*out*/, + const char* publicId) = 0; + + virtual bool SelectPatientToRecycle(int64_t& internalId /*out*/) = 0; + + virtual bool SelectPatientToRecycle(int64_t& internalId /*out*/, + int64_t patientIdToAvoid) = 0; + + virtual void SetGlobalProperty(int32_t property, + const char* value) = 0; + + virtual void SetMainDicomTag(int64_t id, + uint16_t group, + uint16_t element, + const char* value) = 0; + + virtual void SetIdentifierTag(int64_t id, + uint16_t group, + uint16_t element, + const char* value) = 0; + + virtual void SetMetadata(int64_t id, + int32_t metadataType, + const char* value) = 0; + + virtual void SetProtectedPatient(int64_t internalId, + bool isProtected) = 0; + + virtual void StartTransaction() = 0; + + virtual void RollbackTransaction() = 0; + + virtual void CommitTransaction() = 0; + }; + + + + class DatabaseBackendAdapter + { + private: + // This class cannot be instantiated + DatabaseBackendAdapter() + { + } + + static void LogError(IDatabaseBackend* backend, + const std::runtime_error& e) + { + backend->GetOutput().LogError("Exception in database back-end: " + std::string(e.what())); + } + + + static int32_t AddAttachment(void* payload, + int64_t id, + const OrthancPluginAttachment* attachment) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + backend->AddAttachment(id, *attachment); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t AttachChild(void* payload, + int64_t parent, + int64_t child) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + backend->AttachChild(parent, child); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t ClearChanges(void* payload) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + backend->ClearChanges(); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t ClearExportedResources(void* payload) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + backend->ClearExportedResources(); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t CreateResource(int64_t* id, + void* payload, + const char* publicId, + OrthancPluginResourceType resourceType) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + *id = backend->CreateResource(publicId, resourceType); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t DeleteAttachment(void* payload, + int64_t id, + int32_t contentType) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + backend->DeleteAttachment(id, contentType); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t DeleteMetadata(void* payload, + int64_t id, + int32_t metadataType) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + backend->DeleteMetadata(id, metadataType); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t DeleteResource(void* payload, + int64_t id) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + backend->DeleteResource(id); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t GetAllPublicIds(OrthancPluginDatabaseContext* context, + void* payload, + OrthancPluginResourceType resourceType) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + std::list<std::string> ids; + backend->GetAllPublicIds(ids, resourceType); + + for (std::list<std::string>::const_iterator + it = ids.begin(); it != ids.end(); ++it) + { + OrthancPluginDatabaseAnswerString(backend->GetOutput().context_, + backend->GetOutput().database_, + it->c_str()); + } + + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t GetChanges(OrthancPluginDatabaseContext* context, + void* payload, + int64_t since, + uint32_t maxResult) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_Change); + + try + { + bool done; + backend->GetChanges(done, since, maxResult); + + if (done) + { + OrthancPluginDatabaseAnswerChangesDone(backend->GetOutput().context_, + backend->GetOutput().database_); + } + + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t GetChildrenInternalId(OrthancPluginDatabaseContext* context, + void* payload, + int64_t id) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + std::list<int64_t> target; + backend->GetChildrenInternalId(target, id); + + for (std::list<int64_t>::const_iterator + it = target.begin(); it != target.end(); ++it) + { + OrthancPluginDatabaseAnswerInt64(backend->GetOutput().context_, + backend->GetOutput().database_, *it); + } + + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t GetChildrenPublicId(OrthancPluginDatabaseContext* context, + void* payload, + int64_t id) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + std::list<std::string> ids; + backend->GetChildrenPublicId(ids, id); + + for (std::list<std::string>::const_iterator + it = ids.begin(); it != ids.end(); ++it) + { + OrthancPluginDatabaseAnswerString(backend->GetOutput().context_, + backend->GetOutput().database_, + it->c_str()); + } + + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t GetExportedResources(OrthancPluginDatabaseContext* context, + void* payload, + int64_t since, + uint32_t maxResult) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_ExportedResource); + + try + { + bool done; + backend->GetExportedResources(done, since, maxResult); + + if (done) + { + OrthancPluginDatabaseAnswerExportedResourcesDone(backend->GetOutput().context_, + backend->GetOutput().database_); + } + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t GetLastChange(OrthancPluginDatabaseContext* context, + void* payload) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_Change); + + try + { + backend->GetLastChange(); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t GetLastExportedResource(OrthancPluginDatabaseContext* context, + void* payload) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_ExportedResource); + + try + { + backend->GetLastExportedResource(); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t GetMainDicomTags(OrthancPluginDatabaseContext* context, + void* payload, + int64_t id) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_DicomTag); + + try + { + backend->GetMainDicomTags(id); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t GetPublicId(OrthancPluginDatabaseContext* context, + void* payload, + int64_t id) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + std::string s = backend->GetPublicId(id); + OrthancPluginDatabaseAnswerString(backend->GetOutput().context_, + backend->GetOutput().database_, + s.c_str()); + + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t GetResourceCount(uint64_t* target, + void* payload, + OrthancPluginResourceType resourceType) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + *target = backend->GetResourceCount(resourceType); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t GetResourceType(OrthancPluginResourceType* resourceType, + void* payload, + int64_t id) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + *resourceType = backend->GetResourceType(id); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t GetTotalCompressedSize(uint64_t* target, + void* payload) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + *target = backend->GetTotalCompressedSize(); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t GetTotalUncompressedSize(uint64_t* target, + void* payload) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + *target = backend->GetTotalUncompressedSize(); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t IsExistingResource(int32_t* existing, + void* payload, + int64_t id) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + *existing = backend->IsExistingResource(id); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t IsProtectedPatient(int32_t* isProtected, + void* payload, + int64_t id) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + *isProtected = backend->IsProtectedPatient(id); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t ListAvailableMetadata(OrthancPluginDatabaseContext* context, + void* payload, + int64_t id) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + std::list<int32_t> target; + backend->ListAvailableMetadata(target, id); + + for (std::list<int32_t>::const_iterator + it = target.begin(); it != target.end(); ++it) + { + OrthancPluginDatabaseAnswerInt32(backend->GetOutput().context_, + backend->GetOutput().database_, + *it); + } + + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t ListAvailableAttachments(OrthancPluginDatabaseContext* context, + void* payload, + int64_t id) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + std::list<int32_t> target; + backend->ListAvailableAttachments(target, id); + + for (std::list<int32_t>::const_iterator + it = target.begin(); it != target.end(); ++it) + { + OrthancPluginDatabaseAnswerInt32(backend->GetOutput().context_, + backend->GetOutput().database_, + *it); + } + + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t LogChange(void* payload, + const OrthancPluginChange* change) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + backend->LogChange(*change); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t LogExportedResource(void* payload, + const OrthancPluginExportedResource* exported) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + backend->LogExportedResource(*exported); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t LookupAttachment(OrthancPluginDatabaseContext* context, + void* payload, + int64_t id, + int32_t contentType) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_Attachment); + + try + { + backend->LookupAttachment(id, contentType); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t LookupGlobalProperty(OrthancPluginDatabaseContext* context, + void* payload, + int32_t property) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + std::string s; + if (backend->LookupGlobalProperty(s, property)) + { + OrthancPluginDatabaseAnswerString(backend->GetOutput().context_, + backend->GetOutput().database_, + s.c_str()); + } + + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t LookupIdentifier(OrthancPluginDatabaseContext* context, + void* payload, + const OrthancPluginDicomTag* tag) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + std::list<int64_t> target; + backend->LookupIdentifier(target, tag->group, tag->element, tag->value); + + for (std::list<int64_t>::const_iterator + it = target.begin(); it != target.end(); ++it) + { + OrthancPluginDatabaseAnswerInt64(backend->GetOutput().context_, + backend->GetOutput().database_, *it); + } + + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t LookupIdentifier2(OrthancPluginDatabaseContext* context, + void* payload, + const char* value) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + std::list<int64_t> target; + backend->LookupIdentifier(target, value); + + for (std::list<int64_t>::const_iterator + it = target.begin(); it != target.end(); ++it) + { + OrthancPluginDatabaseAnswerInt64(backend->GetOutput().context_, + backend->GetOutput().database_, *it); + } + + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t LookupMetadata(OrthancPluginDatabaseContext* context, + void* payload, + int64_t id, + int32_t metadata) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + std::string s; + if (backend->LookupMetadata(s, id, metadata)) + { + OrthancPluginDatabaseAnswerString(backend->GetOutput().context_, + backend->GetOutput().database_, s.c_str()); + } + + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t LookupParent(OrthancPluginDatabaseContext* context, + void* payload, + int64_t id) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + int64_t parent; + if (backend->LookupParent(parent, id)) + { + OrthancPluginDatabaseAnswerInt64(backend->GetOutput().context_, + backend->GetOutput().database_, parent); + } + + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t LookupResource(OrthancPluginDatabaseContext* context, + void* payload, + const char* publicId) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + int64_t id; + OrthancPluginResourceType type; + if (backend->LookupResource(id, type, publicId)) + { + OrthancPluginDatabaseAnswerResource(backend->GetOutput().context_, + backend->GetOutput().database_, + id, type); + } + + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t SelectPatientToRecycle(OrthancPluginDatabaseContext* context, + void* payload) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + int64_t id; + if (backend->SelectPatientToRecycle(id)) + { + OrthancPluginDatabaseAnswerInt64(backend->GetOutput().context_, + backend->GetOutput().database_, id); + } + + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t SelectPatientToRecycle2(OrthancPluginDatabaseContext* context, + void* payload, + int64_t patientIdToAvoid) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + int64_t id; + if (backend->SelectPatientToRecycle(id, patientIdToAvoid)) + { + OrthancPluginDatabaseAnswerInt64(backend->GetOutput().context_, + backend->GetOutput().database_, id); + } + + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t SetGlobalProperty(void* payload, + int32_t property, + const char* value) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + backend->SetGlobalProperty(property, value); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t SetMainDicomTag(void* payload, + int64_t id, + const OrthancPluginDicomTag* tag) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + backend->SetMainDicomTag(id, tag->group, tag->element, tag->value); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t SetIdentifierTag(void* payload, + int64_t id, + const OrthancPluginDicomTag* tag) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + backend->SetIdentifierTag(id, tag->group, tag->element, tag->value); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t SetMetadata(void* payload, + int64_t id, + int32_t metadata, + const char* value) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + backend->SetMetadata(id, metadata, value); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t SetProtectedPatient(void* payload, + int64_t id, + int32_t isProtected) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + backend->SetProtectedPatient(id, (isProtected != 0)); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t StartTransaction(void* payload) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + backend->StartTransaction(); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t RollbackTransaction(void* payload) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + backend->RollbackTransaction(); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t CommitTransaction(void* payload) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + backend->CommitTransaction(); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t Open(void* payload) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + backend->Open(); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + static int32_t Close(void* payload) + { + IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + backend->Close(); + return 0; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return -1; + } + } + + + public: + static void Register(OrthancPluginContext* context, + IDatabaseBackend& backend) + { + OrthancPluginDatabaseBackend params; + memset(¶ms, 0, sizeof(params)); + + params.addAttachment = AddAttachment; + params.attachChild = AttachChild; + params.clearChanges = ClearChanges; + params.clearExportedResources = ClearExportedResources; + params.createResource = CreateResource; + params.deleteAttachment = DeleteAttachment; + params.deleteMetadata = DeleteMetadata; + params.deleteResource = DeleteResource; + params.getAllPublicIds = GetAllPublicIds; + params.getChanges = GetChanges; + params.getChildrenInternalId = GetChildrenInternalId; + params.getChildrenPublicId = GetChildrenPublicId; + params.getExportedResources = GetExportedResources; + params.getLastChange = GetLastChange; + params.getLastExportedResource = GetLastExportedResource; + params.getMainDicomTags = GetMainDicomTags; + params.getPublicId = GetPublicId; + params.getResourceCount = GetResourceCount; + params.getResourceType = GetResourceType; + params.getTotalCompressedSize = GetTotalCompressedSize; + params.getTotalUncompressedSize = GetTotalUncompressedSize; + params.isExistingResource = IsExistingResource; + params.isProtectedPatient = IsProtectedPatient; + params.listAvailableMetadata = ListAvailableMetadata; + params.listAvailableAttachments = ListAvailableAttachments; + params.logChange = LogChange; + params.logExportedResource = LogExportedResource; + params.lookupAttachment = LookupAttachment; + params.lookupGlobalProperty = LookupGlobalProperty; + params.lookupIdentifier = LookupIdentifier; + params.lookupIdentifier2 = LookupIdentifier2; + params.lookupMetadata = LookupMetadata; + params.lookupParent = LookupParent; + params.lookupResource = LookupResource; + params.selectPatientToRecycle = SelectPatientToRecycle; + params.selectPatientToRecycle2 = SelectPatientToRecycle2; + params.setGlobalProperty = SetGlobalProperty; + params.setMainDicomTag = SetMainDicomTag; + params.setIdentifierTag = SetIdentifierTag; + params.setMetadata = SetMetadata; + params.setProtectedPatient = SetProtectedPatient; + params.startTransaction = StartTransaction; + params.rollbackTransaction = RollbackTransaction; + params.commitTransaction = CommitTransaction; + params.open = Open; + params.close = Close; + + OrthancPluginDatabaseContext* database = OrthancPluginRegisterDatabaseBackend(context, ¶ms, &backend); + if (!context) + { + throw std::runtime_error("Unable to register the database backend"); + } + + backend.RegisterOutput(new DatabaseBackendOutput(context, database)); + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/Basic/CMakeLists.txt Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 2.8) + +project(Basic) + +if (${CMAKE_COMPILER_IS_GNUCXX}) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -pedantic -Werror") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -pedantic -Werror") +endif() + +include_directories(${CMAKE_SOURCE_DIR}/../../Include/) +add_library(PluginTest SHARED Plugin.c) + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + # Linking with "pthread" is necessary, otherwise the software crashes + # http://sourceware.org/bugzilla/show_bug.cgi?id=10652#c17 + target_link_libraries(PluginTest pthread dl) +endif()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/Basic/Plugin.c Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,440 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + **/ + + +#include <OrthancCPlugin.h> + +#include <string.h> +#include <stdio.h> + +static OrthancPluginContext* context = NULL; + + +ORTHANC_PLUGINS_API int32_t Callback1(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + char buffer[1024]; + uint32_t i; + + sprintf(buffer, "Callback on URL [%s] with body [%s]\n", url, request->body); + OrthancPluginLogWarning(context, buffer); + + OrthancPluginSetCookie(context, output, "hello", "world"); + OrthancPluginAnswerBuffer(context, output, buffer, strlen(buffer), "text/plain"); + + OrthancPluginLogWarning(context, ""); + + for (i = 0; i < request->groupsCount; i++) + { + sprintf(buffer, " REGEX GROUP %d = [%s]", i, request->groups[i]); + OrthancPluginLogWarning(context, buffer); + } + + OrthancPluginLogWarning(context, ""); + + for (i = 0; i < request->getCount; i++) + { + sprintf(buffer, " GET [%s] = [%s]", request->getKeys[i], request->getValues[i]); + OrthancPluginLogWarning(context, buffer); + } + + OrthancPluginLogWarning(context, ""); + + for (i = 0; i < request->headersCount; i++) + { + sprintf(buffer, " HEADERS [%s] = [%s]", request->headersKeys[i], request->headersValues[i]); + OrthancPluginLogWarning(context, buffer); + } + + OrthancPluginLogWarning(context, ""); + + return 1; +} + + +ORTHANC_PLUGINS_API int32_t Callback2(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + /* Answer with a sample 16bpp image. */ + + uint16_t buffer[256 * 256]; + uint32_t x, y, value; + + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPluginSendMethodNotAllowed(context, output, "GET"); + } + else + { + value = 0; + for (y = 0; y < 256; y++) + { + for (x = 0; x < 256; x++, value++) + { + buffer[value] = value; + } + } + + OrthancPluginCompressAndAnswerPngImage(context, output, OrthancPluginPixelFormat_Grayscale16, + 256, 256, sizeof(uint16_t) * 256, buffer); + } + + return 0; +} + + +ORTHANC_PLUGINS_API int32_t Callback3(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPluginSendMethodNotAllowed(context, output, "GET"); + } + else + { + OrthancPluginMemoryBuffer dicom; + if (!OrthancPluginGetDicomForInstance(context, &dicom, request->groups[0])) + { + /* No error, forward the DICOM file */ + OrthancPluginAnswerBuffer(context, output, dicom.data, dicom.size, "application/dicom"); + + /* Free memory */ + OrthancPluginFreeMemoryBuffer(context, &dicom); + } + } + + return 0; +} + + +ORTHANC_PLUGINS_API int32_t Callback4(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + /* Answer with a sample 8bpp image. */ + + uint8_t buffer[256 * 256]; + uint32_t x, y, value; + + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPluginSendMethodNotAllowed(context, output, "GET"); + } + else + { + value = 0; + for (y = 0; y < 256; y++) + { + for (x = 0; x < 256; x++, value++) + { + buffer[value] = x; + } + } + + OrthancPluginCompressAndAnswerPngImage(context, output, OrthancPluginPixelFormat_Grayscale8, + 256, 256, 256, buffer); + } + + return 0; +} + + +ORTHANC_PLUGINS_API int32_t Callback5(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + /** + * Demonstration the difference between the + * "OrthancPluginRestApiXXX()" and the + * "OrthancPluginRestApiXXXAfterPlugins()" mechanisms to forward + * REST calls. + * + * # curl http://localhost:8042/forward/built-in/system + * # curl http://localhost:8042/forward/plugins/system + * # curl http://localhost:8042/forward/built-in/plugin/image + * => FAILURE (because the "/plugin/image" URI is implemented by this plugin) + * # curl http://localhost:8042/forward/plugins/plugin/image => SUCCESS + **/ + + OrthancPluginMemoryBuffer tmp; + int isBuiltIn, error; + + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPluginSendMethodNotAllowed(context, output, "GET"); + return 0; + } + + isBuiltIn = strcmp("plugins", request->groups[0]); + + if (isBuiltIn) + { + error = OrthancPluginRestApiGet(context, &tmp, request->groups[1]); + } + else + { + printf("ICI1\n"); + error = OrthancPluginRestApiGetAfterPlugins(context, &tmp, request->groups[1]); + printf("ICI2\n"); + } + + if (error) + { + return -1; + } + else + { + OrthancPluginAnswerBuffer(context, output, tmp.data, tmp.size, "application/octet-stream"); + OrthancPluginFreeMemoryBuffer(context, &tmp); + return 0; + } +} + + +ORTHANC_PLUGINS_API int32_t CallbackCreateDicom(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + const char* pathLocator = "\"Path\" : \""; + char info[1024]; + char *id, *eos; + OrthancPluginMemoryBuffer tmp; + + if (request->method != OrthancPluginHttpMethod_Post) + { + OrthancPluginSendMethodNotAllowed(context, output, "POST"); + } + else + { + /* Make POST request to create a new DICOM instance */ + sprintf(info, "{\"PatientName\":\"Test\"}"); + OrthancPluginRestApiPost(context, &tmp, "/tools/create-dicom", info, strlen(info)); + + /** + * Recover the ID of the created instance is constructed by a + * quick-and-dirty parsing of a JSON string. + **/ + id = strstr((char*) tmp.data, pathLocator) + strlen(pathLocator); + eos = strchr(id, '\"'); + eos[0] = '\0'; + + /* Delete the newly created DICOM instance. */ + OrthancPluginRestApiDelete(context, id); + OrthancPluginFreeMemoryBuffer(context, &tmp); + + /* Set some cookie */ + OrthancPluginSetCookie(context, output, "hello", "world"); + + /* Set some HTTP header */ + OrthancPluginSetHttpHeader(context, output, "Cache-Control", "max-age=0, no-cache"); + + OrthancPluginAnswerBuffer(context, output, "OK\n", 3, "text/plain"); + } + + return 0; +} + + +ORTHANC_PLUGINS_API int32_t OnStoredCallback(OrthancPluginDicomInstance* instance, + const char* instanceId) +{ + char buffer[256]; + FILE* fp; + char* json; + static int first = 1; + + sprintf(buffer, "Just received a DICOM instance of size %d and ID %s from AET %s", + (int) OrthancPluginGetInstanceSize(context, instance), instanceId, + OrthancPluginGetInstanceRemoteAet(context, instance)); + + OrthancPluginLogWarning(context, buffer); + + fp = fopen("PluginReceivedInstance.dcm", "wb"); + fwrite(OrthancPluginGetInstanceData(context, instance), + OrthancPluginGetInstanceSize(context, instance), 1, fp); + fclose(fp); + + json = OrthancPluginGetInstanceSimplifiedJson(context, instance); + if (first) + { + /* Only print the first DICOM instance */ + printf("[%s]\n", json); + first = 0; + } + OrthancPluginFreeString(context, json); + + if (OrthancPluginHasInstanceMetadata(context, instance, "ReceptionDate")) + { + printf("Received on [%s]\n", OrthancPluginGetInstanceMetadata(context, instance, "ReceptionDate")); + } + else + { + OrthancPluginLogError(context, "Instance has no reception date, should never happen!"); + } + + return 0; +} + + +ORTHANC_PLUGINS_API int32_t OnChangeCallback(OrthancPluginChangeType changeType, + OrthancPluginResourceType resourceType, + const char* resourceId) +{ + char info[1024]; + OrthancPluginMemoryBuffer tmp; + + sprintf(info, "Change %d on resource %s of type %d", changeType, resourceId, resourceType); + OrthancPluginLogWarning(context, info); + + if (changeType == OrthancPluginChangeType_NewInstance) + { + sprintf(info, "/instances/%s/metadata/AnonymizedFrom", resourceId); + if (OrthancPluginRestApiGet(context, &tmp, info) == 0) + { + sprintf(info, " Instance %s comes from the anonymization of instance", resourceId); + strncat(info, (const char*) tmp.data, tmp.size); + OrthancPluginLogWarning(context, info); + OrthancPluginFreeMemoryBuffer(context, &tmp); + } + } + + return 0; +} + + +ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c) +{ + OrthancPluginMemoryBuffer tmp; + char info[1024], *s; + int counter, i; + + context = c; + OrthancPluginLogWarning(context, "Sample plugin is initializing"); + + /* Check the version of the Orthanc core */ + if (OrthancPluginCheckVersion(c) == 0) + { + sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin", + c->orthancVersion, + ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER); + OrthancPluginLogError(context, info); + return -1; + } + + /* Print some information about Orthanc */ + sprintf(info, "The version of Orthanc is '%s'", context->orthancVersion); + OrthancPluginLogWarning(context, info); + + s = OrthancPluginGetOrthancPath(context); + sprintf(info, " Path to Orthanc: %s", s); + OrthancPluginLogWarning(context, info); + OrthancPluginFreeString(context, s); + + s = OrthancPluginGetOrthancDirectory(context); + sprintf(info, " Directory of Orthanc: %s", s); + OrthancPluginLogWarning(context, info); + OrthancPluginFreeString(context, s); + + s = OrthancPluginGetConfigurationPath(context); + sprintf(info, " Path to configuration file: %s", s); + OrthancPluginLogWarning(context, info); + OrthancPluginFreeString(context, s); + + /* Print the command-line arguments of Orthanc */ + counter = OrthancPluginGetCommandLineArgumentsCount(context); + for (i = 0; i < counter; i++) + { + s = OrthancPluginGetCommandLineArgument(context, i); + sprintf(info, " Command-line argument %d: \"%s\"", i, s); + OrthancPluginLogWarning(context, info); + OrthancPluginFreeString(context, s); + } + + /* Register the callbacks */ + OrthancPluginRegisterRestCallback(context, "/(plu.*)/hello", Callback1); + OrthancPluginRegisterRestCallback(context, "/plu.*/image", Callback2); + OrthancPluginRegisterRestCallback(context, "/plugin/instances/([^/]+)/info", Callback3); + OrthancPluginRegisterRestCallback(context, "/instances/([^/]+)/preview", Callback4); + OrthancPluginRegisterRestCallback(context, "/forward/(built-in)(/.+)", Callback5); + OrthancPluginRegisterRestCallback(context, "/forward/(plugins)(/.+)", Callback5); + OrthancPluginRegisterRestCallback(context, "/plugin/create", CallbackCreateDicom); + + OrthancPluginRegisterOnStoredInstanceCallback(context, OnStoredCallback); + + OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback); + + /* Declare several properties of the plugin */ + OrthancPluginSetRootUri(context, "/plugin/hello"); + OrthancPluginSetDescription(context, "This is the description of the sample plugin that can be seen in Orthanc Explorer."); + OrthancPluginExtendOrthancExplorer(context, "alert('Hello Orthanc! From sample plugin with love.');"); + + /* Make REST requests to the built-in Orthanc API */ + OrthancPluginRestApiGet(context, &tmp, "/changes"); + OrthancPluginFreeMemoryBuffer(context, &tmp); + OrthancPluginRestApiGet(context, &tmp, "/changes?limit=1"); + OrthancPluginFreeMemoryBuffer(context, &tmp); + + /* Play with PUT by defining a new target modality. */ + sprintf(info, "[ \"STORESCP\", \"localhost\", 2000 ]"); + OrthancPluginRestApiPut(context, &tmp, "/modalities/demo", info, strlen(info)); + + /* Play with global properties: A global counter is incremented + each time the plugin starts. */ + s = OrthancPluginGetGlobalProperty(context, 1024, "0"); + sscanf(s, "%d", &counter); + sprintf(info, "Number of times this plugin was started: %d", counter); + OrthancPluginLogWarning(context, info); + counter++; + sprintf(info, "%d", counter); + OrthancPluginSetGlobalProperty(context, 1024, info); + OrthancPluginFreeString(context, s); + + return 0; +} + + +ORTHANC_PLUGINS_API void OrthancPluginFinalize() +{ + OrthancPluginLogWarning(context, "Sample plugin is finalizing"); +} + + +ORTHANC_PLUGINS_API const char* OrthancPluginGetName() +{ + return "sample"; +} + + +ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() +{ + return "1.0"; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/GdcmDecoding/CMakeLists.txt Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,56 @@ +cmake_minimum_required(VERSION 2.8) + +project(GdcmDecoding) + +SET(ALLOW_DOWNLOADS ON CACHE BOOL "Allow CMake to download packages") +SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of Boost") +SET(USE_SYSTEM_GOOGLE_LOG OFF CACHE BOOL "Use the system version of Google Log") + +set(ORTHANC_ROOT ${CMAKE_SOURCE_DIR}/../../..) + +if (${CMAKE_COMPILER_IS_GNUCXX}) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined") +endif() + +include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake) +include(${ORTHANC_ROOT}/Resources/CMake/BoostConfiguration.cmake) +include(${ORTHANC_ROOT}/Resources/CMake/GoogleLogConfiguration.cmake) + +find_package(GDCM REQUIRED) +if (GDCM_FOUND) + include(${GDCM_USE_FILE}) + set(GDCM_LIBRARIES gdcmCommon gdcmMSFF) +else(GDCM_FOUND) + message(FATAL_ERROR "Cannot find GDCM, did you set GDCM_DIR?") +endif(GDCM_FOUND) + +include_directories( + ${ORTHANC_ROOT}/Plugins/Include/ + ${ORTHANC_ROOT}/OrthancCppClient/SharedLibrary/Laaw + ) + +add_library(GdcmDecoding SHARED + Plugin.cpp + OrthancContext.cpp + + # Sources from Orthanc + ${GOOGLE_LOG_SOURCES} + ${ORTHANC_ROOT}/Core/ChunkedBuffer.cpp + ${ORTHANC_ROOT}/Core/Enumerations.cpp + ${ORTHANC_ROOT}/Core/ImageFormats/ImageAccessor.cpp + ${ORTHANC_ROOT}/Core/ImageFormats/ImageBuffer.cpp + ${ORTHANC_ROOT}/Core/ImageFormats/ImageProcessing.cpp + ${ORTHANC_ROOT}/Core/OrthancException.cpp + ${ORTHANC_ROOT}/Core/Toolbox.cpp + ${ORTHANC_ROOT}/Resources/ThirdParty/base64/base64.cpp + ${ORTHANC_ROOT}/Resources/ThirdParty/md5/md5.c + ${THIRD_PARTY_SOURCES} + ) + +target_link_libraries(GdcmDecoding ${GDCM_LIBRARIES}) + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + target_link_libraries(GdcmDecoding pthread dl rt) +endif()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/GdcmDecoding/OrthancContext.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,158 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + **/ + + +#include "OrthancContext.h" + +#include <stdexcept> + + +void OrthancContext::Check() +{ + if (context_ == NULL) + { + throw std::runtime_error("The Orthanc plugin context is not initialized"); + } +} + + +OrthancContext& OrthancContext::GetInstance() +{ + static OrthancContext instance; + return instance; +} + + +void OrthancContext::ExtractGetArguments(Arguments& arguments, + const OrthancPluginHttpRequest& request) +{ + Check(); + arguments.clear(); + + for (uint32_t i = 0; i < request.getCount; i++) + { + arguments[request.getKeys[i]] = request.getValues[i]; + } +} + + +void OrthancContext::LogError(const std::string& s) +{ + Check(); + OrthancPluginLogError(context_, s.c_str()); +} + + +void OrthancContext::LogWarning(const std::string& s) +{ + Check(); + OrthancPluginLogWarning(context_, s.c_str()); +} + + +void OrthancContext::LogInfo(const std::string& s) +{ + Check(); + OrthancPluginLogInfo(context_, s.c_str()); +} + + +void OrthancContext::Register(const std::string& uri, + OrthancPluginRestCallback callback) +{ + Check(); + OrthancPluginRegisterRestCallback(context_, uri.c_str(), callback); +} + + +void OrthancContext::GetDicomForInstance(std::string& result, + const std::string& instanceId) +{ + Check(); + OrthancPluginMemoryBuffer buffer; + + if (OrthancPluginGetDicomForInstance(context_, &buffer, instanceId.c_str())) + { + throw std::runtime_error("No DICOM instance with Orthanc ID: " + instanceId); + } + + if (buffer.size == 0) + { + result.clear(); + } + else + { + result.assign(reinterpret_cast<char*>(buffer.data), buffer.size); + } + + OrthancPluginFreeMemoryBuffer(context_, &buffer); +} + + +void OrthancContext::CompressAndAnswerPngImage(OrthancPluginRestOutput* output, + const Orthanc::ImageAccessor& accessor) +{ + Check(); + + OrthancPluginPixelFormat format; + switch (accessor.GetFormat()) + { + case Orthanc::PixelFormat_Grayscale8: + format = OrthancPluginPixelFormat_Grayscale8; + break; + + case Orthanc::PixelFormat_Grayscale16: + format = OrthancPluginPixelFormat_Grayscale16; + break; + + case Orthanc::PixelFormat_SignedGrayscale16: + format = OrthancPluginPixelFormat_SignedGrayscale16; + break; + + case Orthanc::PixelFormat_RGB24: + format = OrthancPluginPixelFormat_RGB24; + break; + + case Orthanc::PixelFormat_RGBA32: + format = OrthancPluginPixelFormat_RGBA32; + break; + + default: + throw std::runtime_error("Unsupported pixel format"); + } + + OrthancPluginCompressAndAnswerPngImage(context_, output, format, accessor.GetWidth(), + accessor.GetHeight(), accessor.GetPitch(), accessor.GetConstBuffer()); +} + + + +void OrthancContext::Redirect(OrthancPluginRestOutput* output, + const std::string& s) +{ + Check(); + OrthancPluginRedirect(context_, output, s.c_str()); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/GdcmDecoding/OrthancContext.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,85 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + **/ + + +#pragma once + +#include <OrthancCPlugin.h> + +#include "../../../Core/ImageFormats/ImageBuffer.h" + +#include <map> +#include <string> +#include <boost/noncopyable.hpp> + + +class OrthancContext : public boost::noncopyable +{ +private: + OrthancPluginContext* context_; + + OrthancContext() : context_(NULL) + { + } + + void Check(); + +public: + typedef std::map<std::string, std::string> Arguments; + + static OrthancContext& GetInstance(); + + void Initialize(OrthancPluginContext* context) + { + context_ = context; + } + + void Finalize() + { + context_ = NULL; + } + + void ExtractGetArguments(Arguments& arguments, + const OrthancPluginHttpRequest& request); + + void LogError(const std::string& s); + + void LogWarning(const std::string& s); + + void LogInfo(const std::string& s); + + void Register(const std::string& uri, + OrthancPluginRestCallback callback); + + void GetDicomForInstance(std::string& result, + const std::string& instanceId); + + void CompressAndAnswerPngImage(OrthancPluginRestOutput* output, + const Orthanc::ImageAccessor& accessor); + + void Redirect(OrthancPluginRestOutput* output, + const std::string& s); +};
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/GdcmDecoding/Plugin.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,271 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + **/ + + +#include <map> +#include <string> +#include <stdexcept> +#include <sstream> +#include <boost/lexical_cast.hpp> + +#include "OrthancContext.h" +#include "../../../Core/ImageFormats/ImageProcessing.h" + +#include <gdcmReader.h> +#include <gdcmImageReader.h> +#include <gdcmImageChangePlanarConfiguration.h> + + +static void AnswerUnsupportedImage(OrthancPluginRestOutput* output) +{ + OrthancContext::GetInstance().Redirect(output, "/app/images/unsupported.png"); +} + + +static bool GetOrthancPixelFormat(Orthanc::PixelFormat& format, + const gdcm::Image& image) +{ + if (image.GetPlanarConfiguration() != 0 && + image.GetPixelFormat().GetSamplesPerPixel() != 1) + { + OrthancContext::GetInstance().LogError("Planar configurations are not supported"); + return false; + } + + if (image.GetPixelFormat().GetSamplesPerPixel() == 1) + { + switch (image.GetPixelFormat().GetScalarType()) + { + case gdcm::PixelFormat::UINT8: + format = Orthanc::PixelFormat_Grayscale8; + return true; + + case gdcm::PixelFormat::UINT16: + format = Orthanc::PixelFormat_Grayscale16; + return true; + + case gdcm::PixelFormat::INT16: + format = Orthanc::PixelFormat_SignedGrayscale16; + return true; + + default: + return false; + } + } + else if (image.GetPixelFormat().GetSamplesPerPixel() == 3 && + image.GetPixelFormat().GetScalarType() == gdcm::PixelFormat::UINT8) + { + format = Orthanc::PixelFormat_RGB24; + return true; + } + else if (image.GetPixelFormat().GetSamplesPerPixel() == 4 && + image.GetPixelFormat().GetScalarType() == gdcm::PixelFormat::UINT8) + { + format = Orthanc::PixelFormat_RGBA32; + return true; + } + + return false; +} + + +ORTHANC_PLUGINS_API int32_t DecodeImage(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + std::string instance(request->groups[0]); + std::string outputFormat(request->groups[1]); + OrthancContext::GetInstance().LogWarning("Using GDCM to decode instance " + instance); + + // Download the request DICOM instance from Orthanc into a memory buffer + std::string dicom; + OrthancContext::GetInstance().GetDicomForInstance(dicom, instance); + + // Prepare a memory stream over the DICOM instance + std::stringstream stream(dicom); + + // Parse the DICOM instance using GDCM + gdcm::ImageReader imageReader; + imageReader.SetStream(stream); + if (!imageReader.Read()) + { + OrthancContext::GetInstance().LogError("GDCM cannot extract an image from this DICOM instance"); + AnswerUnsupportedImage(output); + return 0; + } + + gdcm::Image& image = imageReader.GetImage(); + + + // Log information about the decoded image + char tmp[1024]; + sprintf(tmp, "Image format: %dx%d %s with %d color channel(s)", image.GetRows(), image.GetColumns(), + image.GetPixelFormat().GetScalarTypeAsString(), image.GetPixelFormat().GetSamplesPerPixel()); + OrthancContext::GetInstance().LogWarning(tmp); + + + // Convert planar configuration + gdcm::ImageChangePlanarConfiguration planar; + if (image.GetPlanarConfiguration() != 0 && + image.GetPixelFormat().GetSamplesPerPixel() != 1) + { + OrthancContext::GetInstance().LogWarning("Converting planar configuration to interleaved"); + planar.SetInput(imageReader.GetImage()); + planar.Change(); + image = planar.GetOutput(); + } + + + // Create a read-only accessor to the bitmap decoded by GDCM + Orthanc::PixelFormat format; + if (!GetOrthancPixelFormat(format, image)) + { + OrthancContext::GetInstance().LogError("This sample plugin does not support this image format"); + AnswerUnsupportedImage(output); + return 0; + } + + Orthanc::ImageAccessor decodedImage; + std::vector<char> decodedBuffer(image.GetBufferLength()); + + if (decodedBuffer.size()) + { + image.GetBuffer(&decodedBuffer[0]); + unsigned int pitch = image.GetColumns() * ::Orthanc::GetBytesPerPixel(format); + decodedImage.AssignWritable(format, image.GetColumns(), image.GetRows(), pitch, &decodedBuffer[0]); + } + else + { + // Empty image + decodedImage.AssignWritable(format, 0, 0, 0, NULL); + } + + + // Convert the pixel format from GDCM to the format requested by the REST query + Orthanc::ImageBuffer converted; + converted.SetWidth(decodedImage.GetWidth()); + converted.SetHeight(decodedImage.GetHeight()); + + if (outputFormat == "preview") + { + if (format == Orthanc::PixelFormat_RGB24 || + format == Orthanc::PixelFormat_RGBA32) + { + // Do not rescale color image + converted.SetFormat(Orthanc::PixelFormat_RGB24); + } + else + { + converted.SetFormat(Orthanc::PixelFormat_Grayscale8); + + // Rescale the image to the [0,255] range + int64_t a, b; + Orthanc::ImageProcessing::GetMinMaxValue(a, b, decodedImage); + + float offset = -a; + float scaling = 255.0f / static_cast<float>(b - a); + Orthanc::ImageProcessing::ShiftScale(decodedImage, offset, scaling); + } + } + else + { + if (format == Orthanc::PixelFormat_RGB24 || + format == Orthanc::PixelFormat_RGBA32) + { + // Do not convert color images to grayscale values (this is Orthanc convention) + AnswerUnsupportedImage(output); + return 0; + } + + if (outputFormat == "image-uint8") + { + converted.SetFormat(Orthanc::PixelFormat_Grayscale8); + } + else if (outputFormat == "image-uint16") + { + converted.SetFormat(Orthanc::PixelFormat_Grayscale16); + } + else if (outputFormat == "image-int16") + { + converted.SetFormat(Orthanc::PixelFormat_SignedGrayscale16); + } + else + { + OrthancContext::GetInstance().LogError("Unknown output format: " + outputFormat); + AnswerUnsupportedImage(output); + return 0; + } + } + + Orthanc::ImageAccessor convertedAccessor(converted.GetAccessor()); + Orthanc::ImageProcessing::Convert(convertedAccessor, decodedImage); + + // Compress the converted image as a PNG file + OrthancContext::GetInstance().CompressAndAnswerPngImage(output, convertedAccessor); + + return 0; // Success +} + + +extern "C" +{ + ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context) + { + OrthancContext::GetInstance().Initialize(context); + OrthancContext::GetInstance().LogWarning("Initializing GDCM decoding"); + + // Check the version of the Orthanc core + if (OrthancPluginCheckVersion(context) == 0) + { + OrthancContext::GetInstance().LogError( + "Your version of Orthanc (" + std::string(context->orthancVersion) + + ") must be above " + boost::lexical_cast<std::string>(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER) + + "." + boost::lexical_cast<std::string>(ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER) + + "." + boost::lexical_cast<std::string>(ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER) + + " to run this plugin"); + return -1; + } + + OrthancContext::GetInstance().Register("/instances/([^/]+)/(preview|image-uint8|image-uint16|image-int16)", DecodeImage); + return 0; + } + + ORTHANC_PLUGINS_API void OrthancPluginFinalize() + { + OrthancContext::GetInstance().LogWarning("Finalizing GDCM decoding"); + OrthancContext::GetInstance().Finalize(); + } + + ORTHANC_PLUGINS_API const char* OrthancPluginGetName() + { + return "gdcm-decoding"; + } + + ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() + { + return "1.0"; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/ServeFolders/CMakeLists.txt Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 2.8) + +project(ServeFolders) + +set(ALLOW_DOWNLOADS ON) +set(USE_SYSTEM_JSONCPP OFF) +include(../../../Resources/CMake/DownloadPackage.cmake) +include(../../../Resources/CMake/JsonCppConfiguration.cmake) + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + link_libraries(uuid) + SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pthread") + SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -pthread") +elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + link_libraries(rpcrt4 ws2_32 secur32) + if (CMAKE_COMPILER_IS_GNUCXX) + SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -static-libgcc -static-libstdc++") + endif() +endif () + +if (CMAKE_COMPILER_IS_GNUCXX) + SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--version-script=${CMAKE_SOURCE_DIR}/VersionScript.map -Wl,--no-undefined") +endif() + +include_directories(${CMAKE_SOURCE_DIR}/../../Include/) +add_library(ServeFolders SHARED + Plugin.cpp + ${THIRD_PARTY_SOURCES} + ) + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + # Linking with "pthread" is necessary, otherwise the software crashes + # http://sourceware.org/bugzilla/show_bug.cgi?id=10652#c17 + target_link_libraries(ServeFolders dl rt) +endif()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/ServeFolders/Plugin.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,326 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + **/ + + +#include <OrthancCPlugin.h> + +#include <json/reader.h> +#include <json/value.h> +#include <string.h> +#include <stdio.h> +#include <fstream> +#include <algorithm> +#include <sys/stat.h> + +static OrthancPluginContext* context_ = NULL; +static std::map<std::string, std::string> folders_; +static const char* INDEX_URI = "/app/plugin-serve-folders.html"; + + +static const char* GetMimeType(const std::string& path) +{ + size_t dot = path.find_last_of('.'); + + std::string extension = (dot == std::string::npos) ? "" : path.substr(dot); + std::transform(extension.begin(), extension.end(), extension.begin(), tolower); + + if (extension == ".html") + { + return "text/html"; + } + else if (extension == ".css") + { + return "text/css"; + } + else if (extension == ".js") + { + return "application/javascript"; + } + else if (extension == ".gif") + { + return "image/gif"; + } + else if (extension == ".svg") + { + return "image/svg+xml"; + } + else if (extension == ".json") + { + return "application/json"; + } + else if (extension == ".xml") + { + return "application/xml"; + } + else if (extension == ".png") + { + return "image/png"; + } + else if (extension == ".jpg" || extension == ".jpeg") + { + return "image/jpeg"; + } + else + { + std::string s = "Unknown MIME type for extension: " + extension; + OrthancPluginLogWarning(context_, s.c_str()); + return "application/octet-stream"; + } +} + + + +static bool ReadFile(std::string& content, + const std::string& path) +{ + struct stat s; + if (stat(path.c_str(), &s) != 0 || + !(s.st_mode & S_IFREG)) + { + // Either the path does not exist, or it is not a regular file + return false; + } + + FILE* fp = fopen(path.c_str(), "rb"); + if (fp == NULL) + { + return false; + } + + long size; + + if (fseek(fp, 0, SEEK_END) == -1 || + (size = ftell(fp)) < 0) + { + fclose(fp); + return false; + } + + content.resize(size); + + if (fseek(fp, 0, SEEK_SET) == -1) + { + fclose(fp); + return false; + } + + bool ok = true; + + if (size > 0 && + fread(&content[0], size, 1, fp) != 1) + { + ok = false; + } + + fclose(fp); + + return ok; +} + + +static bool ReadConfiguration(Json::Value& configuration, + OrthancPluginContext* context) +{ + std::string path; + + { + char* pathTmp = OrthancPluginGetConfigurationPath(context); + if (pathTmp == NULL) + { + OrthancPluginLogError(context, "No configuration file is provided"); + return false; + } + + path = std::string(pathTmp); + + OrthancPluginFreeString(context, pathTmp); + } + + std::ifstream f(path.c_str()); + + Json::Reader reader; + if (!reader.parse(f, configuration) || + configuration.type() != Json::objectValue) + { + std::string s = "Unable to parse the configuration file: " + std::string(path); + OrthancPluginLogError(context, s.c_str()); + return false; + } + + return true; +} + + +static int32_t FolderCallback(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPluginSendMethodNotAllowed(context_, output, "GET"); + return 0; + } + + const std::string uri = request->groups[0]; + const std::string item = request->groups[1]; + + std::map<std::string, std::string>::const_iterator found = folders_.find(uri); + if (found == folders_.end()) + { + std::string s = "Unknown URI in plugin server-folders: " + uri; + OrthancPluginLogError(context_, s.c_str()); + OrthancPluginSendHttpStatusCode(context_, output, 404); + return 0; + } + + std::string path = found->second + "/" + item; + const char* mime = GetMimeType(path); + + std::string s; + if (ReadFile(s, path)) + { + const char* resource = s.size() ? s.c_str() : NULL; + OrthancPluginAnswerBuffer(context_, output, resource, s.size(), mime); + } + else + { + std::string s = "Inexistent file in served folder: " + path; + OrthancPluginLogError(context_, s.c_str()); + OrthancPluginSendHttpStatusCode(context_, output, 404); + } + + return 0; +} + + +static int32_t IndexCallback(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPluginSendMethodNotAllowed(context_, output, "GET"); + return 0; + } + + std::string s = "<html><body><h1>Additional folders served by Orthanc</h1><ul>\n"; + + for (std::map<std::string, std::string>::const_iterator + it = folders_.begin(); it != folders_.end(); it++) + { + s += "<li><a href=\"" + it->first + "/index.html\">" + it->first + "</li>\n"; + } + + s += "</ul></body></html>"; + + OrthancPluginAnswerBuffer(context_, output, s.c_str(), s.size(), "text/html"); + + return 0; +} + + + +extern "C" +{ + ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context) + { + context_ = context; + + /* Check the version of the Orthanc core */ + if (OrthancPluginCheckVersion(context_) == 0) + { + char info[1024]; + sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin", + context_->orthancVersion, + ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER); + OrthancPluginLogError(context_, info); + return -1; + } + + OrthancPluginSetDescription(context_, "Serve additional folders with the HTTP server of Orthanc."); + + Json::Value configuration; + if (!ReadConfiguration(configuration, context_)) + { + return -1; + } + + if (configuration.isMember("ServeFolders") && + configuration["ServeFolders"].type() == Json::objectValue) + { + Json::Value::Members members = configuration["ServeFolders"].getMemberNames(); + + // Register the callback for each base URI + for (Json::Value::Members::const_iterator + it = members.begin(); it != members.end(); it++) + { + const std::string& baseUri = *it; + const std::string path = configuration["ServeFolders"][*it].asString(); + const std::string regex = "(" + baseUri + ")/(.*)"; + + if (baseUri.empty() || + *baseUri.rbegin() == '/') + { + std::string message = "The URI of a folder to be server cannot be empty or end with a '/': " + *it; + OrthancPluginLogWarning(context_, message.c_str()); + return -1; + } + + OrthancPluginRegisterRestCallback(context, regex.c_str(), FolderCallback); + folders_[baseUri] = path; + } + + OrthancPluginRegisterRestCallback(context, INDEX_URI, IndexCallback); + OrthancPluginSetRootUri(context, INDEX_URI); + } + else + { + OrthancPluginLogWarning(context_, "No section \"ServeFolders\" in your configuration file: " + "No additional folder will be served!"); + } + + return 0; + } + + + ORTHANC_PLUGINS_API void OrthancPluginFinalize() + { + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetName() + { + return "serve-folders"; + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() + { + return "1.0"; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/ServeFolders/README Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,52 @@ +Introduction +============ + +This sample plugin enables Orthanc to serve additional folders using +its embedded Web server. + + +Compilation for Linux +===================== + +# mkdir Build +# cd Build +# cmake .. +# make + + +Cross-compilation for Windows using MinGW +========================================= + +# mkdir Build +# cd Build +# cmake .. -DCMAKE_TOOLCHAIN_FILE=../../../Resources/MinGWToolchain.cmake +# make + + +Configuration +============= + +First, generate the default configuration of Orthanc: +https://code.google.com/p/orthanc/wiki/OrthancConfiguration + +Then, modify the "Plugins" option to point to the folder containing +the built plugins. + +Finally, create a section "ServeFolders" in the configuration file to +specify which folder you want to serve, and at which URI. For +instance, the following excerpt would load the plugins from the +working directory, then would branch the content of the folder +"/home/jodogne/WWW/fosdem" as the URI "http://localhost:8042/fosdem": + +{ + "Name" : "MyOrthanc", + [...] + "HttpPort" : 8042, + [...] + "Plugins" : [ + "." + ], + "ServeFolders" : { + "/fosdem" : "/home/jodogne/WWW/fosdem" + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/ServeFolders/VersionScript.map Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,12 @@ +# This is a version-script for Orthanc plugins + +{ +global: + OrthancPluginInitialize; + OrthancPluginFinalize; + OrthancPluginGetName; + OrthancPluginGetVersion; + +local: + *; +};
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/StorageArea/CMakeLists.txt Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 2.8) + +project(Basic) + +if (${CMAKE_COMPILER_IS_GNUCXX}) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -pedantic -Werror") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -pedantic -Werror") +endif() + +include_directories(${CMAKE_SOURCE_DIR}/../../Include/) +add_library(PluginTest SHARED Plugin.cpp) + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + # Linking with "pthread" is necessary, otherwise the software crashes + # http://sourceware.org/bugzilla/show_bug.cgi?id=10652#c17 + target_link_libraries(PluginTest pthread dl) +endif()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/StorageArea/Plugin.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,162 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + **/ + + +#include <OrthancCPlugin.h> + +#include <string.h> +#include <stdio.h> +#include <string> + +static OrthancPluginContext* context = NULL; + + +static std::string GetPath(const char* uuid) +{ + return "plugin_" + std::string(uuid); +} + + +static int32_t StorageCreate(const char* uuid, + const void* content, + int64_t size, + OrthancPluginContentType type) +{ + std::string path = GetPath(uuid); + + FILE* fp = fopen(path.c_str(), "wb"); + if (!fp) + { + return -1; + } + + bool ok = fwrite(content, size, 1, fp) == 1; + fclose(fp); + + return ok ? 0 : -1; +} + + +static int32_t StorageRead(void** content, + int64_t* size, + const char* uuid, + OrthancPluginContentType type) +{ + std::string path = GetPath(uuid); + + FILE* fp = fopen(path.c_str(), "rb"); + if (!fp) + { + return -1; + } + + if (fseek(fp, 0, SEEK_END) < 0) + { + fclose(fp); + return -1; + } + + *size = ftell(fp); + + if (fseek(fp, 0, SEEK_SET) < 0) + { + fclose(fp); + return -1; + } + + bool ok = true; + + if (*size == 0) + { + *content = NULL; + } + else + { + *content = malloc(*size); + if (*content == NULL || + fread(*content, *size, 1, fp) != 1) + { + ok = false; + } + } + + fclose(fp); + + return ok ? 0 : -1; +} + + +static int32_t StorageRemove(const char* uuid, + OrthancPluginContentType type) +{ + std::string path = GetPath(uuid); + return remove(path.c_str()); +} + + +extern "C" +{ + ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c) + { + context = c; + OrthancPluginLogWarning(context, "Storage plugin is initializing"); + + /* Check the version of the Orthanc core */ + if (OrthancPluginCheckVersion(c) == 0) + { + char info[1024]; + sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin", + c->orthancVersion, + ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER); + OrthancPluginLogError(context, info); + return -1; + } + + OrthancPluginRegisterStorageArea(context, StorageCreate, StorageRead, StorageRemove); + + return 0; + } + + + ORTHANC_PLUGINS_API void OrthancPluginFinalize() + { + OrthancPluginLogWarning(context, "Storage plugin is finalizing"); + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetName() + { + return "storage"; + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() + { + return "1.0"; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/WebSkeleton/CMakeLists.txt Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 2.8) + +project(WebSkeleton) + +SET(STANDALONE_BUILD ON CACHE BOOL "Standalone build (all the resources are embedded, necessary for releases)") +SET(RESOURCES_ROOT ${CMAKE_SOURCE_DIR}/StaticResources) + +include(Framework/Framework.cmake) + +include_directories(${CMAKE_SOURCE_DIR}/../../Include/) + +add_library(WebSkeleton SHARED + ${AUTOGENERATED_SOURCES} + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/WebSkeleton/Configuration.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,34 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + **/ + + +#pragma once + +#define ORTHANC_PLUGIN_NAME "web-skeleton" + +#define ORTHANC_PLUGIN_VERSION "1.0" + +#define ORTHANC_PLUGIN_WEB_ROOT "/plugin-web-skeleton/"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/WebSkeleton/Framework/EmbedResources.py Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,398 @@ +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics +# Department, University Hospital 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 + +UPCASE_CHECK = True +ARGS = [] +for i in range(len(sys.argv)): + if not sys.argv[i].startswith('--'): + ARGS.append(sys.argv[i]) + elif sys.argv[i].lower() == '--no-upcase-check': + UPCASE_CHECK = False + +if len(ARGS) < 2 or len(ARGS) % 2 != 0: + print ('Usage:') + print ('python %s [--no-upcase-check] <TargetBaseFilename> [ <Name> <Source> ]*' % sys.argv[0]) + exit(-1) + +TARGET_BASE_FILENAME = ARGS[1] +SOURCES = ARGS[2:] + +try: + # Make sure the destination directory exists + os.makedirs(os.path.normpath(os.path.join(TARGET_BASE_FILENAME, '..'))) +except: + pass + + +##################################################################### +## Read each resource file +##################################################################### + +def CheckNoUpcase(s): + global UPCASE_CHECK + if (UPCASE_CHECK and + re.search('[A-Z]', s) != None): + raise Exception("Path in a directory with an upcase letter: %s" % s) + +resources = {} + +counter = 0 +i = 0 +while i < len(SOURCES): + resourceName = SOURCES[i].upper() + pathName = SOURCES[i + 1] + + if not os.path.exists(pathName): + raise Exception("Non existing path: %s" % pathName) + + if resourceName in resources: + raise Exception("Twice the same resource: " + resourceName) + + if os.path.isdir(pathName): + # The resource is a directory: Recursively explore its files + content = {} + for root, dirs, files in os.walk(pathName): + base = os.path.relpath(root, pathName) + for f in files: + if f.find('~') == -1: # Ignore Emacs backup files + if base == '.': + r = f + else: + r = os.path.join(base, f) + + CheckNoUpcase(r) + r = '/' + r.replace('\\', '/') + if r in content: + raise Exception("Twice the same filename (check case): " + r) + + content[r] = { + 'Filename' : os.path.join(root, f), + 'Index' : counter + } + counter += 1 + + resources[resourceName] = { + 'Type' : 'Directory', + 'Files' : content + } + + elif os.path.isfile(pathName): + resources[resourceName] = { + 'Type' : 'File', + 'Index' : counter, + 'Filename' : pathName + } + counter += 1 + + else: + raise Exception("Not a regular file, nor a directory: " + pathName) + + i += 2 + +#pprint.pprint(resources) + + +##################################################################### +## Write .h header +##################################################################### + +header = open(TARGET_BASE_FILENAME + '.h', 'w') + +header.write(""" +#pragma once + +#include <string> +#include <list> + +namespace Orthanc +{ + namespace EmbeddedResources + { + enum FileResourceId + { +""") + +isFirst = True +for name in resources: + if resources[name]['Type'] == 'File': + if isFirst: + isFirst = False + else: + header.write(',\n') + header.write(' %s' % name) + +header.write(""" + }; + + enum DirectoryResourceId + { +""") + +isFirst = True +for name in resources: + if resources[name]['Type'] == 'Directory': + if isFirst: + isFirst = False + else: + header.write(',\n') + header.write(' %s' % name) + +header.write(""" + }; + + const void* GetFileResourceBuffer(FileResourceId id); + size_t GetFileResourceSize(FileResourceId id); + void GetFileResource(std::string& result, FileResourceId id); + + const void* GetDirectoryResourceBuffer(DirectoryResourceId id, const char* path); + size_t GetDirectoryResourceSize(DirectoryResourceId id, const char* path); + void GetDirectoryResource(std::string& result, DirectoryResourceId id, const char* path); + + void ListResources(std::list<std::string>& result, DirectoryResourceId id); + } +} +""") +header.close() + + + +##################################################################### +## Write the resource content in the .cpp source +##################################################################### + +PYTHON_MAJOR_VERSION = sys.version_info[0] + +def WriteResource(cpp, item): + cpp.write(' static const uint8_t resource%dBuffer[] = {' % item['Index']) + + f = open(item['Filename'], "rb") + content = f.read() + f.close() + + # http://stackoverflow.com/a/1035360 + pos = 0 + for b in content: + if PYTHON_MAJOR_VERSION == 2: + c = ord(b[0]) + else: + c = b + + if pos > 0: + cpp.write(', ') + + if (pos % 16) == 0: + cpp.write('\n ') + + if c < 0: + raise Exception("Internal error") + + cpp.write("0x%02x" % c) + pos += 1 + + cpp.write(' };\n') + cpp.write(' static const size_t resource%dSize = %d;\n' % (item['Index'], pos)) + + +cpp = open(TARGET_BASE_FILENAME + '.cpp', 'w') + +print os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) + +cpp.write(""" +#include "%s.h" + +#include <stdint.h> +#include <string.h> +#include <stdexcept> + +namespace Orthanc +{ + namespace EmbeddedResources + { +""" % (os.path.basename(TARGET_BASE_FILENAME))) + + +for name in resources: + if resources[name]['Type'] == 'File': + WriteResource(cpp, resources[name]) + else: + for f in resources[name]['Files']: + WriteResource(cpp, resources[name]['Files'][f]) + + + +##################################################################### +## Write the accessors to the file resources in .cpp +##################################################################### + +cpp.write(""" + const void* GetFileResourceBuffer(FileResourceId id) + { + switch (id) + { +""") +for name in resources: + if resources[name]['Type'] == 'File': + cpp.write(' case %s:\n' % name) + cpp.write(' return resource%dBuffer;\n' % resources[name]['Index']) + +cpp.write(""" + default: + throw std::runtime_error("Unknown resource"); + } + } + + size_t GetFileResourceSize(FileResourceId id) + { + switch (id) + { +""") + +for name in resources: + if resources[name]['Type'] == 'File': + cpp.write(' case %s:\n' % name) + cpp.write(' return resource%dSize;\n' % resources[name]['Index']) + +cpp.write(""" + default: + throw std::runtime_error("Unknown resource"); + } + } +""") + + + +##################################################################### +## Write the accessors to the directory resources in .cpp +##################################################################### + +cpp.write(""" + const void* GetDirectoryResourceBuffer(DirectoryResourceId id, const char* path) + { + switch (id) + { +""") + +for name in resources: + if resources[name]['Type'] == 'Directory': + cpp.write(' case %s:\n' % name) + isFirst = True + for path in resources[name]['Files']: + cpp.write(' if (!strcmp(path, "%s"))\n' % path) + cpp.write(' return resource%dBuffer;\n' % resources[name]['Files'][path]['Index']) + cpp.write(' throw std::runtime_error("Unknown path in a directory resource");\n\n') + +cpp.write(""" default: + throw std::runtime_error("Unknown resource"); + } + } + + size_t GetDirectoryResourceSize(DirectoryResourceId id, const char* path) + { + switch (id) + { +""") + +for name in resources: + if resources[name]['Type'] == 'Directory': + cpp.write(' case %s:\n' % name) + isFirst = True + for path in resources[name]['Files']: + cpp.write(' if (!strcmp(path, "%s"))\n' % path) + cpp.write(' return resource%dSize;\n' % resources[name]['Files'][path]['Index']) + cpp.write(' throw std::runtime_error("Unknown path in a directory resource");\n\n') + +cpp.write(""" default: + throw std::runtime_error("Unknown resource"); + } + } +""") + + + + +##################################################################### +## List the resources in a directory +##################################################################### + +cpp.write(""" + void ListResources(std::list<std::string>& result, DirectoryResourceId id) + { + result.clear(); + + switch (id) + { +""") + +for name in resources: + if resources[name]['Type'] == 'Directory': + cpp.write(' case %s:\n' % name) + for path in sorted(resources[name]['Files']): + cpp.write(' result.push_back("%s");\n' % path) + cpp.write(' break;\n\n') + +cpp.write(""" default: + throw std::runtime_error("Unknown resource"); + } + } +""") + + + + +##################################################################### +## Write the convenience wrappers in .cpp +##################################################################### + +cpp.write(""" + void GetFileResource(std::string& result, FileResourceId id) + { + size_t size = GetFileResourceSize(id); + result.resize(size); + if (size > 0) + memcpy(&result[0], GetFileResourceBuffer(id), size); + } + + void GetDirectoryResource(std::string& result, DirectoryResourceId id, const char* path) + { + size_t size = GetDirectoryResourceSize(id, path); + result.resize(size); + if (size > 0) + memcpy(&result[0], GetDirectoryResourceBuffer(id, path), size); + } + } +} +""") +cpp.close()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/WebSkeleton/Framework/Framework.cmake Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,76 @@ +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics +# Department, University Hospital 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/>. + + +if (${CMAKE_COMPILER_IS_GNUCXX}) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -pedantic -Werror -Wno-unused-function") +endif() + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + # Linking with "pthread" is necessary, otherwise the software might crash + # http://sourceware.org/bugzilla/show_bug.cgi?id=10652#c17 + link_libraries(pthread dl) +endif() + +if (STANDALONE_BUILD) + add_definitions(-DORTHANC_PLUGIN_STANDALONE=1) + + set(AUTOGENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/AUTOGENERATED") + set(AUTOGENERATED_SOURCES "${AUTOGENERATED_DIR}/EmbeddedResources.cpp") + + file(MAKE_DIRECTORY ${AUTOGENERATED_DIR}) + include_directories(${AUTOGENERATED_DIR}) + + set(TARGET_BASE "${AUTOGENERATED_DIR}/EmbeddedResources") + add_custom_command( + OUTPUT + "${AUTOGENERATED_DIR}/EmbeddedResources.h" + "${AUTOGENERATED_DIR}/EmbeddedResources.cpp" + COMMAND + python + "${CMAKE_CURRENT_SOURCE_DIR}/Framework/EmbedResources.py" + "${AUTOGENERATED_DIR}/EmbeddedResources" + STATIC_RESOURCES + ${RESOURCES_ROOT} + DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/Framework/EmbedResources.py" + ${STATIC_RESOURCES} + ) + +else() + add_definitions( + -DORTHANC_PLUGIN_STANDALONE=0 + -DORTHANC_PLUGIN_RESOURCES_ROOT="${RESOURCES_ROOT}" + ) +endif() + + +list(APPEND AUTOGENERATED_SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/Framework/Plugin.cpp" + ) \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/WebSkeleton/Framework/Plugin.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,276 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + **/ + + +#include "../Configuration.h" + +#include <OrthancCPlugin.h> +#include <string> +#include <stdexcept> +#include <algorithm> +#include <sys/stat.h> + +#if ORTHANC_PLUGIN_STANDALONE == 1 +// This is an auto-generated file for standalone builds +#include <EmbeddedResources.h> +#endif + +static OrthancPluginContext* context = NULL; + + +static const char* GetMimeType(const std::string& path) +{ + size_t dot = path.find_last_of('.'); + + std::string extension = (dot == std::string::npos) ? "" : path.substr(dot); + std::transform(extension.begin(), extension.end(), extension.begin(), tolower); + + if (extension == ".html") + { + return "text/html"; + } + else if (extension == ".css") + { + return "text/css"; + } + else if (extension == ".js") + { + return "application/javascript"; + } + else if (extension == ".gif") + { + return "image/gif"; + } + else if (extension == ".json") + { + return "application/json"; + } + else if (extension == ".xml") + { + return "application/xml"; + } + else if (extension == ".png") + { + return "image/png"; + } + else if (extension == ".jpg" || extension == ".jpeg") + { + return "image/jpeg"; + } + else + { + std::string s = "Unknown MIME type for extension: " + extension; + OrthancPluginLogWarning(context, s.c_str()); + return "application/octet-stream"; + } +} + + +static bool ReadFile(std::string& content, + const std::string& path) +{ + struct stat s; + if (stat(path.c_str(), &s) != 0 || + !(s.st_mode & S_IFREG)) + { + // Either the path does not exist, or it is not a regular file + return false; + } + + FILE* fp = fopen(path.c_str(), "rb"); + if (fp == NULL) + { + return false; + } + + long size; + + if (fseek(fp, 0, SEEK_END) == -1 || + (size = ftell(fp)) < 0) + { + fclose(fp); + return false; + } + + content.resize(size); + + if (fseek(fp, 0, SEEK_SET) == -1) + { + fclose(fp); + return false; + } + + bool ok = true; + + if (size > 0 && + fread(&content[0], size, 1, fp) != 1) + { + ok = false; + } + + fclose(fp); + + return ok; +} + + +#if ORTHANC_PLUGIN_STANDALONE == 1 +static int32_t ServeStaticResource(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPluginSendMethodNotAllowed(context, output, "GET"); + return 0; + } + + std::string path = "/" + std::string(request->groups[0]); + const char* mime = GetMimeType(path); + + try + { + std::string s; + Orthanc::EmbeddedResources::GetDirectoryResource + (s, Orthanc::EmbeddedResources::STATIC_RESOURCES, path.c_str()); + + const char* resource = s.size() ? s.c_str() : NULL; + OrthancPluginAnswerBuffer(context, output, resource, s.size(), mime); + + return 0; + } + catch (std::runtime_error&) + { + std::string s = "Unknown static resource in plugin: " + std::string(request->groups[0]); + OrthancPluginLogError(context, s.c_str()); + OrthancPluginSendHttpStatusCode(context, output, 404); + return 0; + } +} +#endif + + +#if ORTHANC_PLUGIN_STANDALONE == 0 +static int32_t ServeFolder(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPluginSendMethodNotAllowed(context, output, "GET"); + return 0; + } + + std::string path = ORTHANC_PLUGIN_RESOURCES_ROOT "/" + std::string(request->groups[0]); + const char* mime = GetMimeType(path); + + std::string s; + if (ReadFile(s, path)) + { + const char* resource = s.size() ? s.c_str() : NULL; + OrthancPluginAnswerBuffer(context, output, resource, s.size(), mime); + + return 0; + } + else + { + std::string s = "Unknown static resource in plugin: " + std::string(request->groups[0]); + OrthancPluginLogError(context, s.c_str()); + OrthancPluginSendHttpStatusCode(context, output, 404); + return 0; + } +} +#endif + + +static int32_t RedirectRoot(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPluginSendMethodNotAllowed(context, output, "GET"); + } + else + { + OrthancPluginRedirect(context, output, ORTHANC_PLUGIN_WEB_ROOT "index.html"); + } + + return 0; +} + + +extern "C" +{ + ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c) + { + context = c; + + /* Check the version of the Orthanc core */ + if (OrthancPluginCheckVersion(c) == 0) + { + char info[256]; + sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin", + c->orthancVersion, + ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER); + OrthancPluginLogError(context, info); + return -1; + } + + /* Register the callbacks */ + +#if ORTHANC_PLUGIN_STANDALONE == 1 + OrthancPluginLogInfo(context, "Serving static resources (standalone build)"); + OrthancPluginRegisterRestCallback(context, ORTHANC_PLUGIN_WEB_ROOT "(.*)", ServeStaticResource); +#else + OrthancPluginLogInfo(context, "Serving resources from folder: " ORTHANC_PLUGIN_RESOURCES_ROOT); + OrthancPluginRegisterRestCallback(context, ORTHANC_PLUGIN_WEB_ROOT "(.*)", ServeFolder); +#endif + + OrthancPluginRegisterRestCallback(context, "/", RedirectRoot); + + return 0; + } + + + ORTHANC_PLUGINS_API void OrthancPluginFinalize() + { + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetName() + { + return ORTHANC_PLUGIN_NAME; + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() + { + return ORTHANC_PLUGIN_VERSION; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/WebSkeleton/NOTES.txt Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,7 @@ +This is a sample Orthanc plugin that serves static resources (HTML, +JavaScript, CSS, images...). + +The resources to serve must be stored in the folder "StaticResources". + +The folder "Framework" contains a reusable framework for any plugin +whose sole objective is to serve static resources.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/WebSkeleton/StaticResources/index.html Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,16 @@ +<!doctype html> + +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Skeleton</title> + </head> + + <body> + <h1>Web Skeleton</h1> + <p> + This is a sample skeleton for Orthanc showing how to create a + plugin that serves static HTML resources. + </p> + </body> +</html>
--- a/README Wed Jun 25 15:34:40 2014 +0200 +++ b/README Thu May 21 16:58:30 2015 +0200 @@ -41,11 +41,11 @@ exception: http://people.gnome.org/~markmc/openssl-and-the-gpl.html -We also kindly require scientific works and clinical studies that make -use of Orthanc to cite Orthanc in their associated -publications. Similarly, we require open-source and closed-source -products that make use of Orthanc to warn us about this use. You can -cite our work using the following BibTeX entry: +We also kindly ask scientific works and clinical studies that make +use of Orthanc to cite Orthanc in their associated publications. +Similarly, we ask open-source and closed-source products that make +use of Orthanc to warn us about this use. You can cite our work +using the following BibTeX entry: @inproceedings{Jodogne:ISBI2013, author = {Jodogne, S. and Bernard, C. and Devillers, M. and Lenaerts, E. and Coucke, P.}, @@ -78,8 +78,9 @@ * OrthancCppClient/ - Code of the C++ client * OrthancExplorer/ - Code of the Web application (HTML5/Javascript) * OrthancServer/ - Code of the Orthanc server (depends on DCMTK) +* Plugins/ - Code of the plugin framework * Resources/ - Scripts, resources for building third-party code -* UnitTests/ - Unit tests +* UnitTestsSources/ - Unit tests This archive contains the following files:
--- a/Resources/CMake/AutoGeneratedCode.cmake Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/CMake/AutoGeneratedCode.cmake Thu May 21 16:58:30 2015 +0200 @@ -26,7 +26,7 @@ "${TARGET_BASE}.h" "${TARGET_BASE}.cpp" COMMAND - python + ${PYTHON_EXECUTABLE} "${CMAKE_CURRENT_SOURCE_DIR}/Resources/EmbedResources.py" "${AUTOGENERATED_DIR}/EmbeddedResources" ${SCRIPT_ARGUMENTS}
--- a/Resources/CMake/BoostConfiguration.cmake Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/CMake/BoostConfiguration.cmake Thu May 21 16:58:30 2015 +0200 @@ -56,6 +56,7 @@ if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD") list(APPEND BOOST_SOURCES ${BOOST_SOURCES_DIR}/libs/thread/src/pthread/once.cpp @@ -76,9 +77,17 @@ ${BOOST_SOURCES_DIR}/libs/thread/src/win32/tss_pe.cpp ${BOOST_FILESYSTEM_SOURCES_DIR}/windows_file_codecvt.cpp ) - add_definitions( - -DBOOST_LOCALE_WITH_WCONV=1 - ) + + # Starting with release 0.8.2, Orthanc statically links against + # libiconv, even on Windows. Indeed, the "WCONV" library of + # Windows XP seems not to support properly several codepages + # (notably "Latin3", "Hebrew", and "Arabic"). + + if (USE_BOOST_ICONV) + include(${ORTHANC_ROOT}/Resources/CMake/LibIconvConfiguration.cmake) + else() + add_definitions(-DBOOST_LOCALE_WITH_WCONV=1) + endif() else() message(FATAL_ERROR "Support your platform here") @@ -90,6 +99,16 @@ ) endif() + if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") + # This is a patch to compile Boost 1.55.0 with Clang 3.4 and later + # (including XCode 5.1). Fixes issue 14 of Orthanc. + # https://trac.macports.org/ticket/42282#comment:10 + execute_process( + COMMAND patch -p0 -N -i ${ORTHANC_ROOT}/Resources/Patches/boost-1.55.0-clang-atomic.patch + WORKING_DIRECTORY ${BOOST_SOURCES_DIR} + ) + endif() + aux_source_directory(${BOOST_SOURCES_DIR}/libs/regex/src BOOST_REGEX_SOURCES) list(APPEND BOOST_SOURCES
--- a/Resources/CMake/Compiler.cmake Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/CMake/Compiler.cmake Thu May 21 16:58:30 2015 +0200 @@ -28,28 +28,21 @@ string(REGEX REPLACE "/MDd" "/MTd" ${flag_var} "${${flag_var}}") endforeach(flag_var) + # Add /Zm256 compiler option to Visual Studio to fix PCH errors + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zm256") + add_definitions( -D_CRT_SECURE_NO_WARNINGS=1 -D_CRT_SECURE_NO_DEPRECATE=1 ) - include_directories(${CMAKE_SOURCE_DIR}/Resources/ThirdParty/VisualStudio) + include_directories(${ORTHANC_ROOT}/Resources/ThirdParty/VisualStudio) link_libraries(netapi32) endif() if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR - ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD") - if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") - SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -I${LSB_PATH}/include") - SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -nostdinc++ -I${LSB_PATH}/include -I${LSB_PATH}/include/c++ -I${LSB_PATH}/include/c++/backward -fpermissive") - SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -L${LSB_LIBPATH}") - endif() - - add_definitions( - -D_LARGEFILE64_SOURCE=1 - -D_FILE_OFFSET_BITS=64 - ) - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--as-needed") + ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--no-undefined") set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined") @@ -58,6 +51,15 @@ set(CMAKE_SHARED_LIBRARY_LINK_CXX_FLAGS "") link_libraries(uuid pthread rt) + if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--as-needed") + add_definitions( + -D_LARGEFILE64_SOURCE=1 + -D_FILE_OFFSET_BITS=64 + ) + link_libraries(dl) + endif() + elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Windows") add_definitions( -DWINVER=0x0501 @@ -79,6 +81,22 @@ endif() +if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") + SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -I${LSB_PATH}/include") + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -nostdinc++ -I${LSB_PATH}/include -I${LSB_PATH}/include/c++ -I${LSB_PATH}/include/c++/backward -fpermissive") + SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -L${LSB_LIBPATH}") +endif() + + +if (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") + # In FreeBSD, the "/usr/local/" folder contains the ports and need to be imported + SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -I/usr/local/include") + SET(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} -I/usr/local/include -I/usr/local/include/jsoncpp") + SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L/usr/local/lib") + SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -L/usr/local/lib") +endif() + + if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") CHECK_INCLUDE_FILES(rpc.h HAVE_UUID_H) else() @@ -89,6 +107,7 @@ message(FATAL_ERROR "Please install the uuid-dev package") endif() + if (${STATIC_BUILD}) add_definitions(-DORTHANC_STATIC=1) else()
--- a/Resources/CMake/DcmtkConfiguration.cmake Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/CMake/DcmtkConfiguration.cmake Thu May 21 16:58:30 2015 +0200 @@ -3,6 +3,7 @@ find_path(DCMTK_DICTIONARY_DIR_AUTO dicom.dic /usr/share/dcmtk /usr/share/libdcmtk2 + /usr/local/share/dcmtk ) message("Autodetected path to the DICOM dictionaries: ${DCMTK_DICTIONARY_DIR_AUTO}") @@ -40,6 +41,10 @@ set(HAVE_PROTOTYPE_GETSOCKNAME 1) endif() + set(DCMTK_PACKAGE_VERSION "3.6.0") + set(DCMTK_PACKAGE_VERSION_SUFFIX "") + set(DCMTK_PACKAGE_VERSION_NUMBER 360) + CONFIGURE_FILE( ${DCMTK_SOURCES_DIR}/CMake/osconfig.h.in ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/osconfig.h) @@ -88,11 +93,18 @@ AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/oflog/libsrc DCMTK_SOURCES) if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD") list(REMOVE_ITEM DCMTK_SOURCES ${DCMTK_SOURCES_DIR}/oflog/libsrc/windebap.cc ${DCMTK_SOURCES_DIR}/oflog/libsrc/winsock.cc ) + + execute_process( + COMMAND patch -p0 -N -i ${ORTHANC_ROOT}/Resources/Patches/dcmtk-linux-speed.patch + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + ) + elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") list(REMOVE_ITEM DCMTK_SOURCES ${DCMTK_SOURCES_DIR}/oflog/libsrc/unixsock.cc @@ -101,7 +113,7 @@ if (${CMAKE_COMPILER_IS_GNUCXX}) # This is a patch for MinGW64 execute_process( - COMMAND patch -p0 -i ${CMAKE_SOURCE_DIR}/Resources/Patches/dcmtk-mingw64.patch + COMMAND patch -p0 -N -i ${ORTHANC_ROOT}/Resources/Patches/dcmtk-mingw64.patch WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) endif() @@ -114,6 +126,10 @@ ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/dcdictbi.cc ) + #set_source_files_properties(${DCMTK_SOURCES} + # PROPERTIES COMPILE_DEFINITIONS + # "PACKAGE_VERSION=\"3.6.0\";PACKAGE_VERSION_NUMBER=\"360\"") + # This fixes crashes related to the destruction of the DCMTK OFLogger # http://support.dcmtk.org/docs-snapshot/file_macros.html add_definitions( @@ -154,7 +170,6 @@ list(APPEND DCMTK_LIBRARIES "${tmp}") include_directories(${DCMTK_INCLUDE_DIR}) - link_libraries(${DCMTK_LIBRARIES}) add_definitions( -DHAVE_CONFIG_H=1
--- a/Resources/CMake/DownloadPackage.cmake Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/CMake/DownloadPackage.cmake Thu May 21 16:58:30 2015 +0200 @@ -15,7 +15,12 @@ ## if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows") - find_program(ZIP_EXECUTABLE 7z PATHS "$ENV{ProgramFiles}/7-Zip") + find_program(ZIP_EXECUTABLE 7z + PATHS + "$ENV{ProgramFiles}/7-Zip" + "$ENV{ProgramW6432}/7-Zip" + ) + if (${ZIP_EXECUTABLE} MATCHES "ZIP_EXECUTABLE-NOTFOUND") message(FATAL_ERROR "Please install the '7-zip' software (http://www.7-zip.org/)") endif()
--- a/Resources/CMake/GoogleLogConfiguration.cmake Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/CMake/GoogleLogConfiguration.cmake Thu May 21 16:58:30 2015 +0200 @@ -30,6 +30,7 @@ if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD") set(ac_cv_have_unistd_h 1) set(ac_cv_have_stdint_h 1) @@ -65,44 +66,54 @@ if (CMAKE_COMPILER_IS_GNUCXX) if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") execute_process( - COMMAND patch utilities.cc ${CMAKE_SOURCE_DIR}/Resources/Patches/glog-utilities-lsb.diff + COMMAND patch -N utilities.cc ${ORTHANC_ROOT}/Resources/Patches/glog-utilities-lsb.diff WORKING_DIRECTORY ${GOOGLE_LOG_SOURCES_DIR}/src ) else() execute_process( - COMMAND patch utilities.cc ${CMAKE_SOURCE_DIR}/Resources/Patches/glog-utilities.diff + COMMAND patch -N utilities.cc ${ORTHANC_ROOT}/Resources/Patches/glog-utilities.diff WORKING_DIRECTORY ${GOOGLE_LOG_SOURCES_DIR}/src ) endif() execute_process( - COMMAND patch port.h ${CMAKE_SOURCE_DIR}/Resources/Patches/glog-port-h.diff + COMMAND patch -N port.h ${ORTHANC_ROOT}/Resources/Patches/glog-port-h.diff WORKING_DIRECTORY ${GOOGLE_LOG_SOURCES_DIR}/src/windows ) execute_process( - COMMAND patch port.cc ${CMAKE_SOURCE_DIR}/Resources/Patches/glog-port-cc.diff + COMMAND patch -N port.cc ${ORTHANC_ROOT}/Resources/Patches/glog-port-cc.diff WORKING_DIRECTORY ${GOOGLE_LOG_SOURCES_DIR}/src/windows ) + + else(${MSVC}) + # https://code.google.com/p/google-glog/issues/detail?id=117 + configure_file( + ${ORTHANC_ROOT}/Resources/Patches/glog-visual-studio-port.h + ${GOOGLE_LOG_SOURCES_DIR}/src/windows/port.h + COPYONLY) + endif() + if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD") if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") # Install the specific configuration for LSB SDK configure_file( - ${CMAKE_SOURCE_DIR}/Resources/CMake/GoogleLogConfigurationLSB.h + ${ORTHANC_ROOT}/Resources/CMake/GoogleLogConfigurationLSB.h ${GOOGLE_LOG_SOURCES_DIR}/src/config.h COPYONLY) elseif ("${CMAKE_SYSTEM_NAME}" STREQUAL "Darwin") # Install the specific configuration for Mac OS configure_file( - ${CMAKE_SOURCE_DIR}/Resources/CMake/GoogleLogConfigurationDarwin.h + ${ORTHANC_ROOT}/Resources/CMake/GoogleLogConfigurationDarwin.h ${GOOGLE_LOG_SOURCES_DIR}/src/config.h COPYONLY) else() configure_file( - ${CMAKE_SOURCE_DIR}/Resources/CMake/GoogleLogConfiguration.h + ${ORTHANC_ROOT}/Resources/CMake/GoogleLogConfiguration.h ${GOOGLE_LOG_SOURCES_DIR}/src/config.h COPYONLY) endif()
--- a/Resources/CMake/GoogleTestConfiguration.cmake Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/CMake/GoogleTestConfiguration.cmake Thu May 21 16:58:30 2015 +0200 @@ -8,10 +8,10 @@ endif() elseif (STATIC_BUILD OR NOT USE_SYSTEM_GOOGLE_TEST) - SET(GTEST_SOURCES_DIR ${CMAKE_BINARY_DIR}/gtest-1.6.0) + set(GTEST_SOURCES_DIR ${CMAKE_BINARY_DIR}/gtest-1.7.0) DownloadPackage( - "4577b49f2973c90bf9ba69aa8166b786" - "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/gtest-1.6.0.zip" + "2d6ec8ccdf5c46b05ba54a9fd1d130d7" + "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/gtest-1.7.0.zip" "${GTEST_SOURCES_DIR}") include_directories( @@ -23,6 +23,11 @@ ${GTEST_SOURCES_DIR}/src/gtest-all.cc ) + # https://code.google.com/p/googletest/issues/detail?id=412 + if (MSVC) # VS2012 does not support tuples correctly yet + add_definitions(/D _VARIADIC_MAX=10) + endif() + else() include(FindGTest) if (NOT GTEST_FOUND)
--- a/Resources/CMake/JsonCppConfiguration.cmake Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/CMake/JsonCppConfiguration.cmake Thu May 21 16:58:30 2015 +0200 @@ -23,7 +23,13 @@ message(FATAL_ERROR "Please install the libjsoncpp-dev package") endif() - include_directories(/usr/include/jsoncpp) + find_path(JSONCPP_INCLUDE_DIR json/reader.h + /usr/include/jsoncpp + /usr/local/include/jsoncpp + ) + + message("JsonCpp include dir: ${JSONCPP_INCLUDE_DIR}") + include_directories(${JSONCPP_INCLUDE_DIR}) link_libraries(jsoncpp) endif()
--- a/Resources/CMake/LibCurlConfiguration.cmake Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/CMake/LibCurlConfiguration.cmake Thu May 21 16:58:30 2015 +0200 @@ -26,7 +26,7 @@ -DCURL_DISABLE_LDAP=1 -DCURL_DISABLE_LDAPS=1 -DCURL_DISABLE_POP3=1 - -DCURL_DISABLE_PROXY=1 + #-DCURL_DISABLE_PROXY=1 -DCURL_DISABLE_RTSP=1 -DCURL_DISABLE_TELNET=1 -DCURL_DISABLE_TFTP=1 @@ -42,6 +42,7 @@ if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD") if ("${CMAKE_SIZEOF_VOID_P}" EQUAL "8") SET(TMP_OS "x86_64")
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/CMake/LibIconvConfiguration.cmake Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,47 @@ +set(LIBICONV_SOURCES_DIR ${CMAKE_BINARY_DIR}/libiconv-1.14) +DownloadPackage( + "e34509b1623cec449dfeb73d7ce9c6c6" + "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/libiconv-1.14.tar.gz" + "${LIBICONV_SOURCES_DIR}") + +# https://groups.google.com/d/msg/android-ndk/AS1nkxnk6m4/EQm09hD1tigJ +add_definitions( + -DBOOST_LOCALE_WITH_ICONV=1 + -DBUILDING_LIBICONV=1 + -DIN_LIBRARY=1 + -DLIBDIR="" + -DICONV_CONST= + ) + +configure_file( + ${LIBICONV_SOURCES_DIR}/srclib/localcharset.h + ${LIBICONV_SOURCES_DIR}/include + COPYONLY) + +set(HAVE_VISIBILITY 0) +set(ICONV_CONST ${ICONV_CONST}) +set(USE_MBSTATE_T 1) +set(BROKEN_WCHAR_H 0) +set(EILSEQ) +set(HAVE_WCHAR_T 1) +configure_file( + ${LIBICONV_SOURCES_DIR}/include/iconv.h.build.in + ${LIBICONV_SOURCES_DIR}/include/iconv.h + ) +unset(HAVE_VISIBILITY) +unset(ICONV_CONST) +unset(USE_MBSTATE_T) +unset(BROKEN_WCHAR_H) +unset(EILSEQ) +unset(HAVE_WCHAR_T) + +include_directories( + ${LIBICONV_SOURCES_DIR}/include + ) + +list(APPEND BOOST_SOURCES + ${LIBICONV_SOURCES_DIR}/lib/iconv.c + ${LIBICONV_SOURCES_DIR}/lib/relocatable.c + ${LIBICONV_SOURCES_DIR}/libcharset/lib/localcharset.c + ${LIBICONV_SOURCES_DIR}/libcharset/lib/relocatable.c + )
--- a/Resources/CMake/MongooseConfiguration.cmake Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/CMake/MongooseConfiguration.cmake Thu May 21 16:58:30 2015 +0200 @@ -1,13 +1,30 @@ if (STATIC_BUILD OR NOT USE_SYSTEM_MONGOOSE) SET(MONGOOSE_SOURCES_DIR ${CMAKE_BINARY_DIR}/mongoose) - DownloadPackage( - "e718fc287b4eb1bd523be3fa00942bb0" - "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/mongoose-3.1.tgz" - "${MONGOOSE_SOURCES_DIR}") + + if (0) + # Use Mongoose 3.1 + DownloadPackage( + "e718fc287b4eb1bd523be3fa00942bb0" + "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/mongoose-3.1.tgz" + "${MONGOOSE_SOURCES_DIR}") + + add_definitions(-DMONGOOSE_USE_CALLBACKS=0) + set(MONGOOSE_PATCH ${ORTHANC_ROOT}/Resources/Patches/mongoose-3.1-patch.diff) + + else() + # Use Mongoose 3.8 + DownloadPackage( + "7e3296295072792cdc3c633f9404e0c3" + "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/mongoose-3.8.tgz" + "${MONGOOSE_SOURCES_DIR}") + + add_definitions(-DMONGOOSE_USE_CALLBACKS=1) + set(MONGOOSE_PATCH ${ORTHANC_ROOT}/Resources/Patches/mongoose-3.8-patch.diff) + endif() # Patch mongoose execute_process( - COMMAND patch mongoose.c ${CMAKE_SOURCE_DIR}/Resources/Patches/mongoose-patch.diff + COMMAND patch -N mongoose.c ${MONGOOSE_PATCH} WORKING_DIRECTORY ${MONGOOSE_SOURCES_DIR} ) @@ -56,5 +73,13 @@ message(FATAL_ERROR "Please install the mongoose-devel package") endif() + if (SYSTEM_MONGOOSE_USE_CALLBACKS) + add_definitions(-DMONGOOSE_USE_CALLBACKS=1) + else() + add_definitions(-DMONGOOSE_USE_CALLBACKS=0) + endif() + link_libraries(mongoose) endif() + +
--- a/Resources/CMake/OpenSslConfiguration.cmake Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/CMake/OpenSslConfiguration.cmake Thu May 21 16:58:30 2015 +0200 @@ -188,7 +188,7 @@ elseif ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") execute_process( - COMMAND patch ui_openssl.c ${CMAKE_SOURCE_DIR}/Resources/Patches/openssl-lsb.diff + COMMAND patch -N ui_openssl.c ${ORTHANC_ROOT}/Resources/Patches/openssl-lsb.diff WORKING_DIRECTORY ${OPENSSL_SOURCES_DIR}/crypto/ui )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/CMake/PugixmlConfiguration.cmake Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,35 @@ +if (USE_PUGIXML) + add_definitions(-DORTHANC_PUGIXML_ENABLED=1) + + if (STATIC_BUILD OR NOT USE_SYSTEM_PUGIXML) + set(PUGIXML_SOURCES_DIR ${CMAKE_BINARY_DIR}/pugixml-1.4) + + DownloadPackage( + "7c56c91cfe3ecdee248a8e4892ef5781" + "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/pugixml-1.4.tar.gz" + "${PUGIXML_SOURCES_DIR}") + + include_directories( + ${PUGIXML_SOURCES_DIR}/src + ) + + set(PUGIXML_SOURCES + ${PUGIXML_SOURCES_DIR}/src/vlog_is_on.cc + ) + + list(APPEND THIRD_PARTY_SOURCES + ${PUGIXML_SOURCES_DIR}/src/pugixml.cpp + ) + + else() + CHECK_INCLUDE_FILE_CXX(pugixml.hpp HAVE_PUGIXML_H) + if (NOT HAVE_PUGIXML_H) + message(FATAL_ERROR "Please install the libpugixml-dev package") + endif() + + link_libraries(pugixml) + endif() + +else() + add_definitions(-DORTHANC_PUGIXML_ENABLED=0) +endif()
--- a/Resources/CMake/SQLiteConfiguration.cmake Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/CMake/SQLiteConfiguration.cmake Thu May 21 16:58:30 2015 +0200 @@ -28,8 +28,14 @@ message(FATAL_ERROR "Please install the libsqlite3-dev package") endif() + find_path(SQLITE_INCLUDE_DIR sqlite3.h + /usr/include + /usr/local/include + ) + message("SQLite include dir: ${SQLITE_INCLUDE_DIR}") + # Autodetection of the version of SQLite - file(STRINGS "/usr/include/sqlite3.h" SQLITE_VERSION_NUMBER1 REGEX "#define SQLITE_VERSION_NUMBER.*$") + file(STRINGS "${SQLITE_INCLUDE_DIR}/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}")
--- a/Resources/CMake/VisualStudioPrecompiledHeaders.cmake Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/CMake/VisualStudioPrecompiledHeaders.cmake Thu May 21 16:58:30 2015 +0200 @@ -1,6 +1,6 @@ macro(ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS PrecompiledHeaders PrecompiledSource Sources) get_filename_component(PrecompiledBasename ${PrecompiledHeaders} NAME_WE) - set(PrecompiledBinary "${CMAKE_CURRENT_BINARY_DIR}/${PrecompiledBasename}.pch") + set(PrecompiledBinary "$(IntDir)/${PrecompiledBasename}.pch") set_source_files_properties(${PrecompiledSource} PROPERTIES COMPILE_FLAGS "/Yc\"${PrecompiledHeaders}\" /Fp\"${PrecompiledBinary}\""
--- a/Resources/Configuration.json Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/Configuration.json Thu May 21 16:58:30 2015 +0200 @@ -28,11 +28,18 @@ // of patients) "MaximumPatientCount" : 0, - // List of paths to the custom Lua scripts to load into this - // instance of Orthanc + // List of paths to the custom Lua scripts that are to be loaded + // into this instance of Orthanc "LuaScripts" : [ ], + // List of paths to the plugins that are to be loaded into this + // instance of Orthanc (e.g. "./libPluginTest.so" for Linux, or + // "./PluginTest.dll" for Windows). These paths can refer to + // folders, in which case they will be scanned non-recursively to + // find shared libraries. + "Plugins" : [ + ], /** @@ -57,6 +64,21 @@ // The DICOM port "DicomPort" : 4242, + // The default encoding that is assumed for DICOM files without + // "SpecificCharacterSet" DICOM tag. The allowed values are "Ascii", + // "Utf8", "Latin1", "Latin2", "Latin3", "Latin4", "Latin5", + // "Cyrillic", "Windows1251", "Arabic", "Greek", "Hebrew", "Thai", + // "Japanese", and "Chinese". + "DefaultEncoding" : "Latin1", + + // The transfer syntaxes that are accepted by Orthanc C-Store SCP + "DeflatedTransferSyntaxAccepted" : true, + "JpegTransferSyntaxAccepted" : true, + "Jpeg2000TransferSyntaxAccepted" : true, + "JpegLosslessTransferSyntaxAccepted" : true, + "JpipTransferSyntaxAccepted" : true, + "Mpeg2TransferSyntaxAccepted" : true, + "RleTransferSyntaxAccepted" : true, /** @@ -100,8 +122,9 @@ /** * A fourth parameter is available to enable patches for a * specific PACS manufacturer. The allowed values are currently - * "Generic" (default value), "ClearCanvas", "MedInria" and - * "Dcm4Chee". This parameter is case-sensitive. + * "Generic" (default value), "StoreScp" (storescp tool from + * DCMTK), "ClearCanvas", "MedInria" and "Dcm4Chee". This + * parameter is case-sensitive. **/ // "clearcanvas" : [ "CLEARCANVAS", "192.168.1.1", 104, "ClearCanvas" ] }, @@ -117,6 +140,11 @@ // "peer2" : [ "http://localhost:8044/" ] }, + // Parameters of the HTTP proxy to be used by Orthanc. If set to the + // empty string, no HTTP proxy is used. For instance: + // "HttpProxy" : "192.168.0.1:3128" + // "HttpProxy" : "proxyUser:proxyPassword@192.168.0.1:3128" + "HttpProxy" : "", /** @@ -169,5 +197,30 @@ // The maximum number of results for a single C-FIND request at the // Instance level. Setting this option to "0" means no limit. - "LimitFindInstances" : 0 + "LimitFindInstances" : 0, + + // The maximum number of active jobs in the Orthanc scheduler. When + // this limit is reached, the addition of new jobs is blocked until + // some job finishes. + "LimitJobs" : 10, + + // If this option is set to "false", Orthanc will not log the + // resources that are exported to other DICOM modalities of Orthanc + // peers in the URI "/exports". This is useful to prevent the index + // to grow indefinitely in auto-routing tasks. + "LogExportedResources" : true, + + // Enable or disable HTTP Keep-Alive (deprecated). Set this option + // to "true" only in the case of high HTTP loads. + "KeepAlive" : false, + + // If this option is set to "false", Orthanc will run in index-only + // mode. The DICOM files will not be stored on the drive. + "StoreDicom" : true, + + // DICOM associations are kept open as long as new DICOM commands + // are issued. This option sets the number of seconds of inactivity + // to wait before automatically closing a DICOM association. If set + // to 0, the connection is closed immediately. + "DicomAssociationCloseDelay" : 5 }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/DicomConformanceStatement.py Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,31 @@ +#!/usr/bin/python + +# This file injects the UID information into the DICOM conformance +# statement of Orthanc + +import re + +# Read the conformance statement of Orthanc +with open('DicomConformanceStatement.txt', 'r') as f: + statements = f.readlines() + +# Create an index of all the DICOM UIDs that are known to DCMTK +uids = {} +with open('/usr/include/dcmtk/dcmdata/dcuid.h', 'r') as dcmtk: + for l in dcmtk.readlines(): + m = re.match(r'#define UID_(.+?)\s*"(.+?)"', l) + if m != None: + uids[m.group(1)] = m.group(2) + +# Loop over the lines of the statement, looking for the "|" separator +with open('/tmp/DicomConformanceStatement.txt', 'w') as f: + for l in statements: + m = re.match(r'(\s*)(.*?)(\s*)\|.*$', l) + if m != None: + name = m.group(2) + uid = uids[name] + f.write('%s%s%s| %s\n' % (m.group(1), name, m.group(3), uid)) + + else: + # No "|" in this line, just output it + f.write(l)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/DicomConformanceStatement.txt Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,254 @@ +====================================== +DICOM Conformance Statement of Orthanc +====================================== + + +--------------------- +Echo SCP Conformance +--------------------- + +Orthanc supports the following SOP Classes as an SCP for C-Echo: + + VerificationSOPClass | 1.2.840.10008.1.1 + + +--------------------- +Store SCP Conformance +--------------------- + +Orthanc supports the following SOP Classes as an SCP for C-Store: + + AmbulatoryECGWaveformStorage | 1.2.840.10008.5.1.4.1.1.9.1.3 + ArterialPulseWaveformStorage | 1.2.840.10008.5.1.4.1.1.9.5.1 + AutorefractionMeasurementsStorage | 1.2.840.10008.5.1.4.1.1.78.2 + BasicStructuredDisplayStorage | 1.2.840.10008.5.1.4.1.1.131 + BasicTextSRStorage | 1.2.840.10008.5.1.4.1.1.88.11 + BasicVoiceAudioWaveformStorage | 1.2.840.10008.5.1.4.1.1.9.4.1 + BlendingSoftcopyPresentationStateStorage | 1.2.840.10008.5.1.4.1.1.11.4 + BreastTomosynthesisImageStorage | 1.2.840.10008.5.1.4.1.1.13.1.3 + CardiacElectrophysiologyWaveformStorage | 1.2.840.10008.5.1.4.1.1.9.3.1 + ChestCADSRStorage | 1.2.840.10008.5.1.4.1.1.88.65 + ColonCADSRStorage | 1.2.840.10008.5.1.4.1.1.88.69 + ColorSoftcopyPresentationStateStorage | 1.2.840.10008.5.1.4.1.1.11.2 + ComprehensiveSRStorage | 1.2.840.10008.5.1.4.1.1.88.33 + ComputedRadiographyImageStorage | 1.2.840.10008.5.1.4.1.1.1 + CTImageStorage | 1.2.840.10008.5.1.4.1.1.2 + DeformableSpatialRegistrationStorage | 1.2.840.10008.5.1.4.1.1.66.3 + DigitalIntraOralXRayImageStorageForPresentation | 1.2.840.10008.5.1.4.1.1.1.3 + DigitalIntraOralXRayImageStorageForProcessing | 1.2.840.10008.5.1.4.1.1.1.3.1 + DigitalMammographyXRayImageStorageForPresentation | 1.2.840.10008.5.1.4.1.1.1.2 + DigitalMammographyXRayImageStorageForProcessing | 1.2.840.10008.5.1.4.1.1.1.2.1 + DigitalXRayImageStorageForPresentation | 1.2.840.10008.5.1.4.1.1.1.1 + DigitalXRayImageStorageForProcessing | 1.2.840.10008.5.1.4.1.1.1.1.1 + EncapsulatedCDAStorage | 1.2.840.10008.5.1.4.1.1.104.2 + EncapsulatedPDFStorage | 1.2.840.10008.5.1.4.1.1.104.1 + EnhancedCTImageStorage | 1.2.840.10008.5.1.4.1.1.2.1 + EnhancedMRColorImageStorage | 1.2.840.10008.5.1.4.1.1.4.3 + EnhancedMRImageStorage | 1.2.840.10008.5.1.4.1.1.4.1 + EnhancedPETImageStorage | 1.2.840.10008.5.1.4.1.1.130 + EnhancedSRStorage | 1.2.840.10008.5.1.4.1.1.88.22 + EnhancedUSVolumeStorage | 1.2.840.10008.5.1.4.1.1.6.2 + EnhancedXAImageStorage | 1.2.840.10008.5.1.4.1.1.12.1.1 + EnhancedXRFImageStorage | 1.2.840.10008.5.1.4.1.1.12.2.1 + GeneralAudioWaveformStorage | 1.2.840.10008.5.1.4.1.1.9.4.2 + GeneralECGWaveformStorage | 1.2.840.10008.5.1.4.1.1.9.1.2 + GenericImplantTemplateStorage | 1.2.840.10008.5.1.4.43.1 + GrayscaleSoftcopyPresentationStateStorage | 1.2.840.10008.5.1.4.1.1.11.1 + HemodynamicWaveformStorage | 1.2.840.10008.5.1.4.1.1.9.2.1 + ImplantAssemblyTemplateStorage | 1.2.840.10008.5.1.4.44.1 + ImplantationPlanSRDocumentStorage | 1.2.840.10008.5.1.4.1.1.88.70 + ImplantTemplateGroupStorage | 1.2.840.10008.5.1.4.45.1 + IntraocularLensCalculationsStorage | 1.2.840.10008.5.1.4.1.1.78.8 + KeratometryMeasurementsStorage | 1.2.840.10008.5.1.4.1.1.78.3 + KeyObjectSelectionDocumentStorage | 1.2.840.10008.5.1.4.1.1.88.59 + LensometryMeasurementsStorage | 1.2.840.10008.5.1.4.1.1.78.1 + MacularGridThicknessAndVolumeReportStorage | 1.2.840.10008.5.1.4.1.1.79.1 + MammographyCADSRStorage | 1.2.840.10008.5.1.4.1.1.88.50 + MRImageStorage | 1.2.840.10008.5.1.4.1.1.4 + MRSpectroscopyStorage | 1.2.840.10008.5.1.4.1.1.4.2 + MultiframeGrayscaleByteSecondaryCaptureImageStorage | 1.2.840.10008.5.1.4.1.1.7.2 + MultiframeGrayscaleWordSecondaryCaptureImageStorage | 1.2.840.10008.5.1.4.1.1.7.3 + MultiframeSingleBitSecondaryCaptureImageStorage | 1.2.840.10008.5.1.4.1.1.7.1 + MultiframeTrueColorSecondaryCaptureImageStorage | 1.2.840.10008.5.1.4.1.1.7.4 + NuclearMedicineImageStorage | 1.2.840.10008.5.1.4.1.1.20 + OphthalmicAxialMeasurementsStorage | 1.2.840.10008.5.1.4.1.1.78.7 + OphthalmicPhotography16BitImageStorage | 1.2.840.10008.5.1.4.1.1.77.1.5.2 + OphthalmicPhotography8BitImageStorage | 1.2.840.10008.5.1.4.1.1.77.1.5.1 + OphthalmicTomographyImageStorage | 1.2.840.10008.5.1.4.1.1.77.1.5.4 + OphthalmicVisualFieldStaticPerimetryMeasurementsStorage | 1.2.840.10008.5.1.4.1.1.80.1 + PositronEmissionTomographyImageStorage | 1.2.840.10008.5.1.4.1.1.128 + ProcedureLogStorage | 1.2.840.10008.5.1.4.1.1.88.40 + PseudoColorSoftcopyPresentationStateStorage | 1.2.840.10008.5.1.4.1.1.11.3 + RawDataStorage | 1.2.840.10008.5.1.4.1.1.66 + RealWorldValueMappingStorage | 1.2.840.10008.5.1.4.1.1.67 + RespiratoryWaveformStorage | 1.2.840.10008.5.1.4.1.1.9.6.1 + RTBeamsTreatmentRecordStorage | 1.2.840.10008.5.1.4.1.1.481.4 + RTBrachyTreatmentRecordStorage | 1.2.840.10008.5.1.4.1.1.481.6 + RTDoseStorage | 1.2.840.10008.5.1.4.1.1.481.2 + RTImageStorage | 1.2.840.10008.5.1.4.1.1.481.1 + RTIonBeamsTreatmentRecordStorage | 1.2.840.10008.5.1.4.1.1.481.9 + RTIonPlanStorage | 1.2.840.10008.5.1.4.1.1.481.8 + RTPlanStorage | 1.2.840.10008.5.1.4.1.1.481.5 + RTStructureSetStorage | 1.2.840.10008.5.1.4.1.1.481.3 + RTTreatmentSummaryRecordStorage | 1.2.840.10008.5.1.4.1.1.481.7 + SecondaryCaptureImageStorage | 1.2.840.10008.5.1.4.1.1.7 + SegmentationStorage | 1.2.840.10008.5.1.4.1.1.66.4 + SpatialFiducialsStorage | 1.2.840.10008.5.1.4.1.1.66.2 + SpatialRegistrationStorage | 1.2.840.10008.5.1.4.1.1.66.1 + SpectaclePrescriptionReportStorage | 1.2.840.10008.5.1.4.1.1.78.6 + StereometricRelationshipStorage | 1.2.840.10008.5.1.4.1.1.77.1.5.3 + SubjectiveRefractionMeasurementsStorage | 1.2.840.10008.5.1.4.1.1.78.4 + SurfaceSegmentationStorage | 1.2.840.10008.5.1.4.1.1.66.5 + TwelveLeadECGWaveformStorage | 1.2.840.10008.5.1.4.1.1.9.1.1 + UltrasoundImageStorage | 1.2.840.10008.5.1.4.1.1.6.1 + UltrasoundMultiframeImageStorage | 1.2.840.10008.5.1.4.1.1.3.1 + VideoEndoscopicImageStorage | 1.2.840.10008.5.1.4.1.1.77.1.1.1 + VideoMicroscopicImageStorage | 1.2.840.10008.5.1.4.1.1.77.1.2.1 + VideoPhotographicImageStorage | 1.2.840.10008.5.1.4.1.1.77.1.4.1 + VisualAcuityMeasurementsStorage | 1.2.840.10008.5.1.4.1.1.78.5 + VLEndoscopicImageStorage | 1.2.840.10008.5.1.4.1.1.77.1.1 + VLMicroscopicImageStorage | 1.2.840.10008.5.1.4.1.1.77.1.2 + VLPhotographicImageStorage | 1.2.840.10008.5.1.4.1.1.77.1.4 + VLSlideCoordinatesMicroscopicImageStorage | 1.2.840.10008.5.1.4.1.1.77.1.3 + VLWholeSlideMicroscopyImageStorage | 1.2.840.10008.5.1.4.1.1.77.1.6 + XAXRFGrayscaleSoftcopyPresentationStateStorage | 1.2.840.10008.5.1.4.1.1.11.5 + XRay3DAngiographicImageStorage | 1.2.840.10008.5.1.4.1.1.13.1.1 + XRay3DCraniofacialImageStorage | 1.2.840.10008.5.1.4.1.1.13.1.2 + XRayAngiographicImageStorage | 1.2.840.10008.5.1.4.1.1.12.1 + XRayRadiationDoseSRStorage | 1.2.840.10008.5.1.4.1.1.88.67 + XRayRadiofluoroscopicImageStorage | 1.2.840.10008.5.1.4.1.1.12.2 + + RETIRED_HardcopyColorImageStorage | 1.2.840.10008.5.1.1.30 + RETIRED_HardcopyGrayscaleImageStorage | 1.2.840.10008.5.1.1.29 + RETIRED_NuclearMedicineImageStorage | 1.2.840.10008.5.1.4.1.1.5 + RETIRED_StandaloneCurveStorage | 1.2.840.10008.5.1.4.1.1.9 + RETIRED_StandaloneModalityLUTStorage | 1.2.840.10008.5.1.4.1.1.10 + RETIRED_StandaloneOverlayStorage | 1.2.840.10008.5.1.4.1.1.8 + RETIRED_StandalonePETCurveStorage | 1.2.840.10008.5.1.4.1.1.129 + RETIRED_StandaloneVOILUTStorage | 1.2.840.10008.5.1.4.1.1.11 + RETIRED_StoredPrintStorage | 1.2.840.10008.5.1.1.27 + RETIRED_UltrasoundImageStorage | 1.2.840.10008.5.1.4.1.1.6 + RETIRED_UltrasoundMultiframeImageStorage | 1.2.840.10008.5.1.4.1.1.3 + RETIRED_VLImageStorage | 1.2.840.10008.5.1.4.1.1.77.1 + RETIRED_VLMultiFrameImageStorage | 1.2.840.10008.5.1.4.1.1.77.2 + RETIRED_XRayAngiographicBiPlaneImageStorage | 1.2.840.10008.5.1.4.1.1.12.3 + + DRAFT_SRAudioStorage | 1.2.840.10008.5.1.4.1.1.88.2 + DRAFT_SRComprehensiveStorage | 1.2.840.10008.5.1.4.1.1.88.4 + DRAFT_SRDetailStorage | 1.2.840.10008.5.1.4.1.1.88.3 + DRAFT_SRTextStorage | 1.2.840.10008.5.1.4.1.1.88.1 + DRAFT_WaveformStorage | 1.2.840.10008.5.1.4.1.1.9.1 + DRAFT_RTBeamsDeliveryInstructionStorage | 1.2.840.10008.5.1.4.34.1 + + +-------------------- +Find SCP Conformance +-------------------- + +Orthanc supports the following SOP Classes as an SCP for C-Find: + + FINDPatientRootQueryRetrieveInformationModel | 1.2.840.10008.5.1.4.1.2.1.1 + FINDStudyRootQueryRetrieveInformationModel | 1.2.840.10008.5.1.4.1.2.2.1 + + +-------------------- +Move SCP Conformance +-------------------- + +Orthanc supports the following SOP Classes as an SCP for C-Move: + + MOVEPatientRootQueryRetrieveInformationModel | 1.2.840.10008.5.1.4.1.2.1.2 + MOVEStudyRootQueryRetrieveInformationModel | 1.2.840.10008.5.1.4.1.2.2.2 + + +--------------------- +Echo SCU Conformance +--------------------- + +Orthanc supports the following SOP Classes as an SCU for C-Echo: + + VerificationSOPClass | 1.2.840.10008.1.1 + + +--------------------- +Store SCU Conformance +--------------------- + +All the SOP Classes that are listed in the "Store SCP Conformance" +(see above) section are available as an SCU for C-Store. + + +-------------------- +Find SCU Conformance +-------------------- + +Orthanc supports the following SOP Classes as an SCU for C-Find: + + FINDPatientRootQueryRetrieveInformationModel | 1.2.840.10008.5.1.4.1.2.1.1 + FINDStudyRootQueryRetrieveInformationModel | 1.2.840.10008.5.1.4.1.2.2.1 + FINDStudyRootQueryRetrieveInformationModel | 1.2.840.10008.5.1.4.1.2.2.1 + + +----------------- +Transfer Syntaxes +----------------- + +Orthanc will accept and negociate presentation contexts for all of the +abovementioned supported SOP Classes using any of the following +transfer syntaxes: + + LittleEndianImplicitTransferSyntax | 1.2.840.10008.1.2 + LittleEndianExplicitTransferSyntax | 1.2.840.10008.1.2.1 + BigEndianExplicitTransferSyntax | 1.2.840.10008.1.2.2 + DeflatedExplicitVRLittleEndianTransferSyntax | 1.2.840.10008.1.2.1.99 + JPEGProcess1TransferSyntax | 1.2.840.10008.1.2.4.50 + JPEGProcess2_4TransferSyntax | 1.2.840.10008.1.2.4.51 + JPEGProcess3_5TransferSyntax | 1.2.840.10008.1.2.4.52 + JPEGProcess6_8TransferSyntax | 1.2.840.10008.1.2.4.53 + JPEGProcess7_9TransferSyntax | 1.2.840.10008.1.2.4.54 + JPEGProcess10_12TransferSyntax | 1.2.840.10008.1.2.4.55 + JPEGProcess11_13TransferSyntax | 1.2.840.10008.1.2.4.56 + JPEGProcess14TransferSyntax | 1.2.840.10008.1.2.4.57 + JPEGProcess15TransferSyntax | 1.2.840.10008.1.2.4.58 + JPEGProcess16_18TransferSyntax | 1.2.840.10008.1.2.4.59 + JPEGProcess17_19TransferSyntax | 1.2.840.10008.1.2.4.60 + JPEGProcess20_22TransferSyntax | 1.2.840.10008.1.2.4.61 + JPEGProcess21_23TransferSyntax | 1.2.840.10008.1.2.4.62 + JPEGProcess24_26TransferSyntax | 1.2.840.10008.1.2.4.63 + JPEGProcess25_27TransferSyntax | 1.2.840.10008.1.2.4.64 + JPEGProcess28TransferSyntax | 1.2.840.10008.1.2.4.65 + JPEGProcess29TransferSyntax | 1.2.840.10008.1.2.4.66 + JPEGProcess14SV1TransferSyntax | 1.2.840.10008.1.2.4.70 + JPEGLSLosslessTransferSyntax | 1.2.840.10008.1.2.4.80 + JPEGLSLossyTransferSyntax | 1.2.840.10008.1.2.4.81 + JPEG2000LosslessOnlyTransferSyntax | 1.2.840.10008.1.2.4.90 + JPEG2000TransferSyntax | 1.2.840.10008.1.2.4.91 + JPEG2000Part2MulticomponentImageCompressionLosslessOnlyTransferSyntax | 1.2.840.10008.1.2.4.92 + JPEG2000Part2MulticomponentImageCompressionTransferSyntax | 1.2.840.10008.1.2.4.93 + JPIPReferencedTransferSyntax | 1.2.840.10008.1.2.4.94 + JPIPReferencedDeflateTransferSyntax | 1.2.840.10008.1.2.4.95 + MPEG2MainProfileAtMainLevelTransferSyntax | 1.2.840.10008.1.2.4.100 + MPEG2MainProfileAtHighLevelTransferSyntax | 1.2.840.10008.1.2.4.101 + RLELosslessTransferSyntax | 1.2.840.10008.1.2.5 + +It is possible to disable a subset of these transfer syntaxes thanks to the +"*TransferSyntaxAccepted" options in the Orthanc configuration file. + +When possible, Orthanc will prefer the +LittleEndianImplicitTransferSyntax transfer syntax +(1.2.840.10008.1.2). + +Orthanc does not support extended negotiation. + + +-------------------- +Implementation notes +-------------------- + +The information above about the SCP support is readily extracted from +the function "Orthanc::Internals::AcceptAssociation()" from file +"OrthancServer/Internals/CommandDispatcher.cpp". + +The information above about the SCU support is derived from the class +"Orthanc::DicomUserConnection" from file +"OrthancServer/DicomProtocol/DicomUserConnection.cpp".
--- a/Resources/EmbedResources.py Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/EmbedResources.py Thu May 21 16:58:30 2015 +0200 @@ -1,6 +1,6 @@ # Orthanc - A Lightweight, RESTful DICOM Store -# Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, -# Belgium +# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics +# Department, University Hospital 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 @@ -86,6 +86,13 @@ content = {} for root, dirs, files in os.walk(pathName): base = os.path.relpath(root, pathName) + + # Fix issue #24 (Build fails on OSX when directory has .DS_Store files): + # Ignore folders whose name starts with a dot (".") + if base.find('/.') != -1: + print('Ignoring folder: %s' % root) + continue + for f in files: if f.find('~') == -1: # Ignore Emacs backup files if base == '.':
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/EncodingTests.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,61 @@ +static const unsigned int testEncodingsCount = 18; +static const ::Orthanc::Encoding testEncodings[] = { + ::Orthanc::Encoding_Latin5, + ::Orthanc::Encoding_Hebrew, + ::Orthanc::Encoding_Greek, + ::Orthanc::Encoding_Arabic, + ::Orthanc::Encoding_Cyrillic, + ::Orthanc::Encoding_Latin4, + ::Orthanc::Encoding_Latin3, + ::Orthanc::Encoding_Latin2, + ::Orthanc::Encoding_Latin1, + ::Orthanc::Encoding_Utf8, + ::Orthanc::Encoding_Thai, + ::Orthanc::Encoding_Japanese, + ::Orthanc::Encoding_Ascii, + ::Orthanc::Encoding_Windows1251, + ::Orthanc::Encoding_Chinese, + ::Orthanc::Encoding_Windows1251, + ::Orthanc::Encoding_Windows1251, + ::Orthanc::Encoding_Windows1251 +}; +static const char *testEncodingsEncoded[18] = { + "\x54\x65\x73\x74\xe9\xe4\xf6\xf2\xdd", + "\x54\x65\x73\x74\xe3", + "\x54\x65\x73\x74\xc8", + "\x54\x65\x73\x74\xd5", + "\x54\x65\x73\x74\xb4\xfb", + "\x54\x65\x73\x74\xe9\xe4\xf6\xf3", + "\x54\x65\x73\x74\xe9\xe4\xf6\xf2\xf8\xa9", + "\x54\x65\x73\x74\xe9\xe4\xf6", + "\x54\x65\x73\x74\xe9\xe4\xf6\xf2", + "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc3\xb2\xd0\x94\xce\x98\xc4\x9d\xd7\x93\xd8\xb5\xc4\xb7\xd1\x9b\xe0\xb9\x9b\xef\xbe\x88\xc4\xb0", + "\x54\x65\x73\x74\xfb", + "\x54\x65\x73\x74\x84\x44\x83\xa6\xc8", + "\x54\x65\x73\x74", + "\x54\x65\x73\x74\xc4\x9e", + "\x81\x30\x89\x37\x81\x30\x89\x38\xA8\xA4\xA8\xA2\x81\x30\x89\x39\x81\x30\x8A\x30", + "\xd0\xe5\xed\xf2\xe3\xe5\xed\xee\xe3\xf0\xe0\xf4\xe8\xff", + "\xD2\xE0\xE7", + "\xcf\xf0\xff\xec\xe0\xff" +}; +static const char *testEncodingsExpected[18] = { + "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc3\xb2\xc4\xb0", + "\x54\x65\x73\x74\xd7\x93", + "\x54\x65\x73\x74\xce\x98", + "\x54\x65\x73\x74\xd8\xb5", + "\x54\x65\x73\x74\xd0\x94\xd1\x9b", + "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc4\xb7", + "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc3\xb2\xc4\x9d\xc4\xb0", + "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6", + "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc3\xb2", + "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc3\xb2\xd0\x94\xce\x98\xc4\x9d\xd7\x93\xd8\xb5\xc4\xb7\xd1\x9b\xe0\xb9\x9b\xef\xbe\x88\xc4\xb0", + "\x54\x65\x73\x74\xe0\xb9\x9b", + "\x54\x65\x73\x74\xd0\x94\xce\x98\xef\xbe\x88", + "\x54\x65\x73\x74", + "\x54\x65\x73\x74\xd0\x94\xd1\x9b", + "\xc3\x9e\xc3\x9f\xc3\xa0\xc3\xa1\xc3\xa2\xc3\xa3", + "\xd0\xa0\xd0\xb5\xd0\xbd\xd1\x82\xd0\xb3\xd0\xb5\xd0\xbd\xd0\xbe\xd0\xb3\xd1\x80\xd0\xb0\xd1\x84\xd0\xb8\xd1\x8f", + "\xd0\xa2\xd0\xb0\xd0\xb7", + "\xd0\x9f\xd1\x80\xd1\x8f\xd0\xbc\xd0\xb0\xd1\x8f" +};
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/EncodingTests.py Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,74 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +source = u'TestéäöòДΘĝדصķћ๛ネİ' + +encodings = { + 'UTF-8' : 'Utf8', + 'ASCII' : 'Ascii', + 'ISO-8859-1' : 'Latin1', + 'ISO-8859-2' : 'Latin2', + 'ISO-8859-3' : 'Latin3', + 'ISO-8859-4' : 'Latin4', + 'ISO-8859-9' : 'Latin5', + 'ISO-8859-5' : 'Cyrillic', + 'WINDOWS-1251' : 'Windows1251', + 'ISO-8859-6' : 'Arabic', + 'ISO-8859-7' : 'Greek', + 'ISO-8859-8' : 'Hebrew', + 'TIS-620' : 'Thai', + 'SHIFT-JIS' : 'Japanese', + #'GB18030' : 'Chinese', # Done manually below (*) +} + +#from encodings.aliases import aliases +#for a, b in aliases.iteritems(): +# print '%s : %s' % (a, b) + + +# "63" corresponds to "?" +l = [] +encoded = [] +expected = [] + +def ToArray(source): + result = '' + for byte in bytearray(source): + result += '\\x%02x' % byte + return '"%s"' % result + + +for encoding, orthancEnumeration in encodings.iteritems(): + l.append('::Orthanc::Encoding_%s' % orthancEnumeration) + s = source.encode(encoding, 'ignore') + encoded.append(ToArray(s)) + expected.append(ToArray(s.decode(encoding).encode('utf-8'))) + + +# https://en.wikipedia.org/wiki/GB_18030#Technical_details (*) +l.append('::Orthanc::Encoding_Chinese') +expected.append(ToArray('Þßàáâã')) +encoded.append('"\\x81\\x30\\x89\\x37\\x81\\x30\\x89\\x38\\xA8\\xA4\\xA8\\xA2\\x81\\x30\\x89\\x39\\x81\\x30\\x8A\\x30"') + +# Issue 32 +# "encoded" is the copy/paste from "dcm2xml +Ca cyrillic Issue32.dcm" +l.append('::Orthanc::Encoding_Windows1251') +encoded.append('"\\xd0\\xe5\\xed\\xf2\\xe3\\xe5\\xed\\xee\\xe3\\xf0\\xe0\\xf4\\xe8\\xff"') +expected.append(ToArray('Рентгенография')) +l.append('::Orthanc::Encoding_Windows1251') +encoded.append('"\\xD2\\xE0\\xE7"') +expected.append(ToArray('Таз')) +l.append('::Orthanc::Encoding_Windows1251') +encoded.append('"\\xcf\\xf0\\xff\\xec\\xe0\\xff"') +expected.append(ToArray('Прямая')) + + +if True: + print 'static const unsigned int testEncodingsCount = %d;' % len(l) + print 'static const ::Orthanc::Encoding testEncodings[] = {\n %s\n};' % (',\n '.join(l)) + print 'static const char *testEncodingsEncoded[%d] = {\n %s\n};' % (len(l), ',\n '.join(encoded)) + print 'static const char *testEncodingsExpected[%d] = {\n %s\n};' % (len(l), ',\n '.join(expected)) +else: + for i in range(len(expected)): + print expected[i] + #print '%s: %s' % (expected[i], l[i])
--- a/Resources/Orthanc.doxygen Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/Orthanc.doxygen Thu May 21 16:58:30 2015 +0200 @@ -941,7 +941,7 @@ # page will contain the date and time when the page was generated. Setting # this to NO can help when comparing the output of multiple runs. -HTML_TIMESTAMP = YES +HTML_TIMESTAMP = NO # If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML # documentation will contain sections that can be hidden and shown after the
--- a/Resources/OrthancClient.doxygen Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/OrthancClient.doxygen Thu May 21 16:58:30 2015 +0200 @@ -939,7 +939,7 @@ # page will contain the date and time when the page was generated. Setting # this to NO can help when comparing the output of multiple runs. -HTML_TIMESTAMP = YES +HTML_TIMESTAMP = NO # If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML # documentation will contain sections that can be hidden and shown after the
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/OrthancPlugin.doxygen Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,1794 @@ +# Doxyfile 1.8.1.2 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (" "). + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the config file +# that follow. The default is UTF-8 which is also the encoding used for all +# text before the first occurrence of this tag. Doxygen uses libiconv (or the +# iconv built into libc) for the transcoding. See +# http://www.gnu.org/software/libiconv for the list of possible encodings. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or sequence of words) that should +# identify the project. Note that if you do not use Doxywizard you need +# to put quotes around the project name if it contains spaces. + +PROJECT_NAME = "Orthanc Plugin SDK" + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. +# This could be handy for archiving the generated documentation or +# if some version control system is used. + +PROJECT_NUMBER = + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer +# a quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = "Documentation of the plugin interface of Orthanc" + +# With the PROJECT_LOGO tag one can specify an logo or icon that is +# included in the documentation. The maximum height of the logo should not +# exceed 55 pixels and the maximum width should not exceed 200 pixels. +# Doxygen will copy the logo to the output directory. + +PROJECT_LOGO = @CMAKE_SOURCE_DIR@/Resources/OrthancLogoDocumentation.png + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) +# base path where the generated documentation will be put. +# If a relative path is entered, it will be relative to the location +# where doxygen was started. If left blank the current directory will be used. + +OUTPUT_DIRECTORY = OrthancPluginDocumentation + +# If the CREATE_SUBDIRS tag is set to YES, then doxygen will create +# 4096 sub-directories (in 2 levels) under the output directory of each output +# format and will distribute the generated files over these directories. +# Enabling this option can be useful when feeding doxygen a huge amount of +# source files, where putting all generated files in the same directory would +# otherwise cause performance problems for the file system. + +CREATE_SUBDIRS = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# The default language is English, other supported languages are: +# Afrikaans, Arabic, Brazilian, Catalan, Chinese, Chinese-Traditional, +# Croatian, Czech, Danish, Dutch, Esperanto, Farsi, Finnish, French, German, +# Greek, Hungarian, Italian, Japanese, Japanese-en (Japanese with English +# messages), Korean, Korean-en, Lithuanian, Norwegian, Macedonian, Persian, +# Polish, Portuguese, Romanian, Russian, Serbian, Serbian-Cyrillic, Slovak, +# Slovene, Spanish, Swedish, Ukrainian, and Vietnamese. + +OUTPUT_LANGUAGE = English + +# If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will +# include brief member descriptions after the members that are listed in +# the file and class documentation (similar to JavaDoc). +# Set to NO to disable this. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend +# the brief description of a member or function before the detailed description. +# Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. + +REPEAT_BRIEF = NO + +# This tag implements a quasi-intelligent brief description abbreviator +# that is used to form the text in various listings. Each string +# in this list, if found as the leading text of the brief description, will be +# stripped from the text and the result after processing the whole list, is +# used as the annotated text. Otherwise, the brief description is used as-is. +# If left blank, the following values are used ("$name" is automatically +# replaced with the name of the entity): "The $name class" "The $name widget" +# "The $name file" "is" "provides" "specifies" "contains" +# "represents" "a" "an" "the" + +ABBREVIATE_BRIEF = + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# Doxygen will generate a detailed section even if there is only a brief +# description. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full +# path before files name in the file list and in the header files. If set +# to NO the shortest path that makes the file name unique will be used. + +FULL_PATH_NAMES = NO + +# If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag +# can be used to strip a user-defined part of the path. Stripping is +# only done if one of the specified strings matches the left-hand part of +# the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the +# path to strip. + +STRIP_FROM_PATH = + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of +# the path mentioned in the documentation of a class, which tells +# the reader which header file to include in order to use a class. +# If left blank only the name of the header file containing the class +# definition is used. Otherwise one should specify the include paths that +# are normally passed to the compiler using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter +# (but less readable) file names. This can be useful if your file system +# doesn't support long names like on DOS, Mac, or CD-ROM. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen +# will interpret the first line (until the first dot) of a JavaDoc-style +# comment as the brief description. If set to NO, the JavaDoc +# comments will behave just like regular Qt-style comments +# (thus requiring an explicit @brief command for a brief description.) + +JAVADOC_AUTOBRIEF = NO + +# If the QT_AUTOBRIEF tag is set to YES then Doxygen will +# interpret the first line (until the first dot) of a Qt-style +# comment as the brief description. If set to NO, the comments +# will behave just like regular Qt-style comments (thus requiring +# an explicit \brief command for a brief description.) + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen +# treat a multi-line C++ special comment block (i.e. a block of //! or /// +# comments) as a brief description. This used to be the default behaviour. +# The new default is to treat a multi-line C++ comment block as a detailed +# description. Set this tag to YES if you prefer the old behaviour instead. + +MULTILINE_CPP_IS_BRIEF = NO + +# If the INHERIT_DOCS tag is set to YES (the default) then an undocumented +# member inherits the documentation from any documented member that it +# re-implements. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce +# a new page for each member. If set to NO, the documentation of a member will +# be part of the file/class/namespace that contains it. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. +# Doxygen uses this value to replace tabs by spaces in code fragments. + +TAB_SIZE = 8 + +# This tag can be used to specify a number of aliases that acts +# as commands in the documentation. An alias has the form "name=value". +# For example adding "sideeffect=\par Side Effects:\n" will allow you to +# put the command \sideeffect (or @sideeffect) in the documentation, which +# will result in a user-defined paragraph with heading "Side Effects:". +# You can put \n's in the value part of an alias to insert newlines. + +ALIASES = + +# This tag can be used to specify a number of word-keyword mappings (TCL only). +# A mapping has the form "name=value". For example adding +# "class=itcl::class" will allow you to use the command class in the +# itcl::class meaning. + +TCL_SUBST = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C +# sources only. Doxygen will then generate output that is more tailored for C. +# For instance, some of the names that are used will be different. The list +# of all members will be omitted, etc. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java +# sources only. Doxygen will then generate output that is more tailored for +# Java. For instance, namespaces will be presented as packages, qualified +# scopes will look different, etc. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources only. Doxygen will then generate output that is more tailored for +# Fortran. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for +# VHDL. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given extension. +# Doxygen has a built-in mapping, but you can override or extend it using this +# tag. The format is ext=language, where ext is a file extension, and language +# is one of the parsers supported by doxygen: IDL, Java, Javascript, CSharp, C, +# C++, D, PHP, Objective-C, Python, Fortran, VHDL, C, C++. For instance to make +# doxygen treat .inc files as Fortran files (default is PHP), and .f files as C +# (default is Fortran), use: inc=Fortran f=C. Note that for custom extensions +# you also need to set FILE_PATTERNS otherwise the files are not read by doxygen. + +EXTENSION_MAPPING = + +# If MARKDOWN_SUPPORT is enabled (the default) then doxygen pre-processes all +# comments according to the Markdown format, which allows for more readable +# documentation. See http://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by doxygen, so you +# can mix doxygen, HTML, and XML commands with Markdown formatting. +# Disable only in case of backward compatibilities issues. + +MARKDOWN_SUPPORT = YES + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should +# set this tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); v.s. +# func(std::string) {}). This also makes the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. + +BUILTIN_STL_SUPPORT = YES + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip sources only. +# Doxygen will parse them like normal C++ but will assume all classes use public +# instead of private inheritance when no explicit protection keyword is present. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate getter +# and setter methods for a property. Setting this option to YES (the default) +# will make doxygen replace the get and set methods by a property in the +# documentation. This will only work if the methods are indeed getting or +# setting a simple type. If this is not the case, or you want to show the +# methods anyway, you should set this option to NO. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES, then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. + +DISTRIBUTE_GROUP_DOC = NO + +# Set the SUBGROUPING tag to YES (the default) to allow class member groups of +# the same type (for instance a group of public functions) to be put as a +# subgroup of that type (e.g. under the Public Functions section). Set it to +# NO to prevent subgrouping. Alternatively, this can be done per class using +# the \nosubgrouping command. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and +# unions are shown inside the group in which they are included (e.g. using +# @ingroup) instead of on a separate page (for HTML and Man pages) or +# section (for LaTeX and RTF). + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and +# unions with only public data fields will be shown inline in the documentation +# of the scope in which they are defined (i.e. file, namespace, or group +# documentation), provided this scope is documented. If set to NO (the default), +# structs, classes, and unions are shown on a separate page (for HTML and Man +# pages) or section (for LaTeX and RTF). + +INLINE_SIMPLE_STRUCTS = NO + +# When TYPEDEF_HIDES_STRUCT is enabled, a typedef of a struct, union, or enum +# is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically +# be useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. + +TYPEDEF_HIDES_STRUCT = NO + +# The SYMBOL_CACHE_SIZE determines the size of the internal cache use to +# determine which symbols to keep in memory and which to flush to disk. +# When the cache is full, less often used symbols will be written to disk. +# For small to medium size projects (<1000 input files) the default value is +# probably good enough. For larger projects a too small cache size can cause +# doxygen to be busy swapping symbols to and from disk most of the time +# causing a significant performance penalty. +# If the system has enough physical memory increasing the cache will improve the +# performance by keeping more symbols in memory. Note that the value works on +# a logarithmic scale so increasing the size by one will roughly double the +# memory usage. The cache size is given by this formula: +# 2^(16+SYMBOL_CACHE_SIZE). The valid range is 0..9, the default is 0, +# corresponding to a cache size of 2^16 = 65536 symbols. + +SYMBOL_CACHE_SIZE = 0 + +# Similar to the SYMBOL_CACHE_SIZE the size of the symbol lookup cache can be +# set using LOOKUP_CACHE_SIZE. This cache is used to resolve symbols given +# their name and scope. Since this can be an expensive process and often the +# same symbol appear multiple times in the code, doxygen keeps a cache of +# pre-resolved symbols. If the cache is too small doxygen will become slower. +# If the cache is too large, memory is wasted. The cache size is given by this +# formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range is 0..9, the default is 0, +# corresponding to a cache size of 2^16 = 65536 symbols. + +LOOKUP_CACHE_SIZE = 0 + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in +# documentation are documented, even if no documentation was available. +# Private class members and static file members will be hidden unless +# the EXTRACT_PRIVATE and EXTRACT_STATIC tags are set to YES + +EXTRACT_ALL = NO + +# If the EXTRACT_PRIVATE tag is set to YES all private members of a class +# will be included in the documentation. + +EXTRACT_PRIVATE = NO + +# If the EXTRACT_PACKAGE tag is set to YES all members with package or internal scope will be included in the documentation. + +EXTRACT_PACKAGE = NO + +# If the EXTRACT_STATIC tag is set to YES all static members of a file +# will be included in the documentation. + +EXTRACT_STATIC = NO + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) +# defined locally in source files will be included in the documentation. +# If set to NO only classes defined in header files are included. + +EXTRACT_LOCAL_CLASSES = YES + +# This flag is only useful for Objective-C code. When set to YES local +# methods, which are defined in the implementation section but not in +# the interface are included in the documentation. +# If set to NO (the default) only methods in the interface are included. + +EXTRACT_LOCAL_METHODS = NO + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base +# name of the file that contains the anonymous namespace. By default +# anonymous namespaces are hidden. + +EXTRACT_ANON_NSPACES = NO + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all +# undocumented members of documented classes, files or namespaces. +# If set to NO (the default) these members will be included in the +# various overviews, but no documentation section is generated. +# This option has no effect if EXTRACT_ALL is enabled. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. +# If set to NO (the default) these classes will be included in the various +# overviews. This option has no effect if EXTRACT_ALL is enabled. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all +# friend (class|struct|union) declarations. +# If set to NO (the default) these declarations will be included in the +# documentation. + +HIDE_FRIEND_COMPOUNDS = YES + +# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any +# documentation blocks found inside the body of a function. +# If set to NO (the default) these blocks will be appended to the +# function's detailed documentation block. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation +# that is typed after a \internal command is included. If the tag is set +# to NO (the default) then the documentation will be excluded. +# Set it to YES to include the internal documentation. + +INTERNAL_DOCS = NO + +# If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate +# file names in lower-case letters. If set to YES upper-case letters are also +# allowed. This is useful if you have classes or files whose names only differ +# in case and if your file system supports case sensitive file names. Windows +# and Mac users are advised to set this option to NO. + +CASE_SENSE_NAMES = YES + +# If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen +# will show members with their full class and namespace scopes in the +# documentation. If set to YES the scope will be hidden. + +HIDE_SCOPE_NAMES = NO + +# If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen +# will put a list of the files that are included by a file in the documentation +# of that file. + +SHOW_INCLUDE_FILES = YES + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then Doxygen +# will list include files with double quotes in the documentation +# rather than with sharp brackets. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES (the default) then a tag [inline] +# is inserted in the documentation for inline members. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen +# will sort the (detailed) documentation of file and class members +# alphabetically by member name. If set to NO the members will appear in +# declaration order. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the +# brief documentation of file, namespace and class members alphabetically +# by member name. If set to NO (the default) the members will appear in +# declaration order. + +SORT_BRIEF_DOCS = NO + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen +# will sort the (brief and detailed) documentation of class members so that +# constructors and destructors are listed first. If set to NO (the default) +# the constructors will appear in the respective orders defined by +# SORT_MEMBER_DOCS and SORT_BRIEF_DOCS. +# This tag will be ignored for brief docs if SORT_BRIEF_DOCS is set to NO +# and ignored for detailed docs if SORT_MEMBER_DOCS is set to NO. + +SORT_MEMBERS_CTORS_1ST = NO + +# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the +# hierarchy of group names into alphabetical order. If set to NO (the default) +# the group names will appear in their defined order. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be +# sorted by fully-qualified names, including namespaces. If set to +# NO (the default), the class list will be sorted only by class name, +# not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the +# alphabetical list. + +SORT_BY_SCOPE_NAME = NO + +# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to +# do proper type resolution of all parameters of a function it will reject a +# match between the prototype and the implementation of a member function even +# if there is only one candidate or it is obvious which candidate to choose +# by doing a simple string match. By disabling STRICT_PROTO_MATCHING doxygen +# will still accept a match between prototype and implementation in such cases. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable (YES) or +# disable (NO) the todo list. This list is created by putting \todo +# commands in the documentation. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable (YES) or +# disable (NO) the test list. This list is created by putting \test +# commands in the documentation. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or +# disable (NO) the bug list. This list is created by putting \bug +# commands in the documentation. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or +# disable (NO) the deprecated list. This list is created by putting +# \deprecated commands in the documentation. + +GENERATE_DEPRECATEDLIST= YES + +# The ENABLED_SECTIONS tag can be used to enable conditional +# documentation sections, marked by \if sectionname ... \endif. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines +# the initial value of a variable or macro consists of for it to appear in +# the documentation. If the initializer consists of more lines than specified +# here it will be hidden. Use a value of 0 to hide initializers completely. +# The appearance of the initializer of individual variables and macros in the +# documentation can be controlled using \showinitializer or \hideinitializer +# command in the documentation regardless of this setting. + +MAX_INITIALIZER_LINES = 0 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated +# at the bottom of the documentation of classes and structs. If set to YES the +# list will mention the files that were used to generate the documentation. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. +# This will remove the Files entry from the Quick Index and from the +# Folder Tree View (if specified). The default is YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the +# Namespaces page. +# This will remove the Namespaces entry from the Quick Index +# and from the Folder Tree View (if specified). The default is YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command <command> <input-file>, where <command> is the value of +# the FILE_VERSION_FILTER tag, and <input-file> is the name of an input file +# provided by doxygen. Whatever the program writes to standard output +# is used as the file version. See the manual for examples. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents doxygen's defaults, run doxygen with the -l option. +# You can optionally specify a file name after the option, if omitted +# DoxygenLayout.xml will be used as the name of the layout file. + +LAYOUT_FILE = + +# The CITE_BIB_FILES tag can be used to specify one or more bib files +# containing the references data. This must be a list of .bib files. The +# .bib extension is automatically appended if omitted. Using this command +# requires the bibtex tool to be installed. See also +# http://en.wikipedia.org/wiki/BibTeX for more info. For LaTeX the style +# of the bibliography can be controlled using LATEX_BIB_STYLE. To use this +# feature you need bibtex and perl available in the search path. + +CITE_BIB_FILES = + +#--------------------------------------------------------------------------- +# configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated +# by doxygen. Possible values are YES and NO. If left blank NO is used. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated by doxygen. Possible values are YES and NO. If left blank +# NO is used. + +WARNINGS = YES + +# If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings +# for undocumented members. If EXTRACT_ALL is set to YES then this flag will +# automatically be disabled. + +WARN_IF_UNDOCUMENTED = YES + +# If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as not documenting some +# parameters in a documented function, or documenting parameters that +# don't exist or using markup commands wrongly. + +WARN_IF_DOC_ERROR = YES + +# The WARN_NO_PARAMDOC option can be enabled to get warnings for +# functions that are documented, but have no documentation for their parameters +# or return value. If set to NO (the default) doxygen will only warn about +# wrong or incomplete parameter documentation, but not about the absence of +# documentation. + +WARN_NO_PARAMDOC = YES + +# The WARN_FORMAT tag determines the format of the warning messages that +# doxygen can produce. The string should contain the $file, $line, and $text +# tags, which will be replaced by the file and line number from which the +# warning originated and the warning text. Optionally the format may contain +# $version, which will be replaced by the version of the file (if it could +# be obtained via FILE_VERSION_FILTER) + +WARN_FORMAT = "$file:$line: $text" + +# The WARN_LOGFILE tag can be used to specify a file to which warning +# and error messages should be written. If left blank the output is written +# to stderr. + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag can be used to specify the files and/or directories that contain +# documented source files. You may enter file names like "myfile.cpp" or +# directories like "/usr/src/myproject". Separate the files or directories +# with spaces. + +INPUT = @CMAKE_SOURCE_DIR@/Plugins/Include/OrthancCPlugin.h \ + @CMAKE_SOURCE_DIR@/Plugins/Include/OrthancCDatabasePlugin.h \ + @CMAKE_SOURCE_DIR@/Plugins/Include/OrthancCppDatabasePlugin.h + +# This tag can be used to specify the character encoding of the source files +# that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is +# also the default input encoding. Doxygen uses libiconv (or the iconv built +# into libc) for the transcoding. See http://www.gnu.org/software/libiconv for +# the list of possible encodings. + +INPUT_ENCODING = UTF-8 + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp +# and *.h) to filter out the source-files in the directories. If left +# blank the following patterns are tested: +# *.c *.cc *.cxx *.cpp *.c++ *.d *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh +# *.hxx *.hpp *.h++ *.idl *.odl *.cs *.php *.php3 *.inc *.m *.mm *.dox *.py +# *.f90 *.f *.for *.vhd *.vhdl + +FILE_PATTERNS = *.h + +# The RECURSIVE tag can be used to turn specify whether or not subdirectories +# should be searched for input files as well. Possible values are YES and NO. +# If left blank NO is used. + +RECURSIVE = YES + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# Note that relative paths are relative to the directory from which doxygen is +# run. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. Note that the wildcards are matched +# against the file with absolute path, so to exclude all test directories +# for example use the pattern */test/* + +EXCLUDE_PATTERNS = + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# AClass::ANamespace, ANamespace::*Test + +EXCLUDE_SYMBOLS = _OrthancPlugin* OrthancPluginGetName + +# The EXAMPLE_PATH tag can be used to specify one or more files or +# directories that contain example code fragments that are included (see +# the \include command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp +# and *.h) to filter out the source-files in the directories. If left +# blank all files are included. + +EXAMPLE_PATTERNS = + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude +# commands irrespective of the value of the RECURSIVE tag. +# Possible values are YES and NO. If left blank NO is used. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or +# directories that contain image that are included in the documentation (see +# the \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command <filter> <input-file>, where <filter> +# is the value of the INPUT_FILTER tag, and <input-file> is the name of an +# input file. Doxygen will then use the output that the filter program writes +# to standard output. +# If FILTER_PATTERNS is specified, this tag will be +# ignored. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. +# Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. +# The filters are a list of the form: +# pattern=filter (like *.cpp=my_cpp_filter). See INPUT_FILTER for further +# info on how filters are used. If FILTER_PATTERNS is empty or if +# non of the patterns match the file name, INPUT_FILTER is applied. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will be used to filter the input files when producing source +# files to browse (i.e. when SOURCE_BROWSER is set to YES). + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) +# and it is also possible to disable source filtering for a specific pattern +# using *.ext= (so without naming a filter). This option only has effect when +# FILTER_SOURCE_FILES is enabled. + +FILTER_SOURCE_PATTERNS = + +#--------------------------------------------------------------------------- +# configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will +# be generated. Documented entities will be cross-referenced with these sources. +# Note: To get rid of all source code in the generated output, make sure also +# VERBATIM_HEADERS is set to NO. + +SOURCE_BROWSER = NO + +# Setting the INLINE_SOURCES tag to YES will include the body +# of functions and classes directly in the documentation. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct +# doxygen to hide any special comment blocks from generated source code +# fragments. Normal C, C++ and Fortran comments will always remain visible. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES +# then for each documented function all documented +# functions referencing it will be listed. + +REFERENCED_BY_RELATION = NO + +# If the REFERENCES_RELATION tag is set to YES +# then for each documented function all documented entities +# called/used by that function will be listed. + +REFERENCES_RELATION = NO + +# If the REFERENCES_LINK_SOURCE tag is set to YES (the default) +# and SOURCE_BROWSER tag is set to YES, then the hyperlinks from +# functions in REFERENCES_RELATION and REFERENCED_BY_RELATION lists will +# link to the source code. +# Otherwise they will link to the documentation. + +REFERENCES_LINK_SOURCE = YES + +# If the USE_HTAGS tag is set to YES then the references to source code +# will point to the HTML generated by the htags(1) tool instead of doxygen +# built-in source browser. The htags tool is part of GNU's global source +# tagging system (see http://www.gnu.org/software/global/global.html). You +# will need version 4.8.6 or higher. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen +# will generate a verbatim copy of the header file for each class for +# which an include is specified. Set to NO to disable this. + +VERBATIM_HEADERS = YES + +#--------------------------------------------------------------------------- +# configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index +# of all compounds will be generated. Enable this if the project +# contains a lot of classes, structs, unions or interfaces. + +ALPHABETICAL_INDEX = YES + +# If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then +# the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns +# in which this list will be split (can be a number in the range [1..20]) + +COLS_IN_ALPHA_INDEX = 5 + +# In case all classes in a project start with a common prefix, all +# classes will be put under the same header in the alphabetical index. +# The IGNORE_PREFIX tag can be used to specify one or more prefixes that +# should be ignored while generating the index headers. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES (the default) Doxygen will +# generate HTML output. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `html' will be used as the default path. + +HTML_OUTPUT = doc + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for +# each generated HTML page (for example: .htm,.php,.asp). If it is left blank +# doxygen will generate files with .html extension. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a personal HTML header for +# each generated HTML page. If it is left blank doxygen will generate a +# standard header. Note that when using a custom header you are responsible +# for the proper inclusion of any scripts and style sheets that doxygen +# needs, which is dependent on the configuration options used. +# It is advised to generate a default header using "doxygen -w html +# header.html footer.html stylesheet.css YourConfigFile" and then modify +# that header. Note that the header is subject to change so you typically +# have to redo this when upgrading to a newer version of doxygen or when +# changing the value of configuration settings such as GENERATE_TREEVIEW! + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a personal HTML footer for +# each generated HTML page. If it is left blank doxygen will generate a +# standard footer. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading +# style sheet that is used by each HTML page. It can be used to +# fine-tune the look of the HTML output. If the tag is left blank doxygen +# will generate a default style sheet. Note that doxygen will try to copy +# the style sheet file to the HTML output directory, so don't put your own +# style sheet in the HTML output directory as well, or it will be erased! + +HTML_STYLESHEET = + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath$ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that +# the files will be copied as-is; there are no commands or markers available. + +HTML_EXTRA_FILES = + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. +# Doxygen will adjust the colors in the style sheet and background images +# according to this color. Hue is specified as an angle on a colorwheel, +# see http://en.wikipedia.org/wiki/Hue for more information. +# For instance the value 0 represents red, 60 is yellow, 120 is green, +# 180 is cyan, 240 is blue, 300 purple, and 360 is red again. +# The allowed range is 0 to 359. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of +# the colors in the HTML output. For a value of 0 the output will use +# grayscales only. A value of 255 will produce the most vivid colors. + +HTML_COLORSTYLE_SAT = 100 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to +# the luminance component of the colors in the HTML output. Values below +# 100 gradually make the output lighter, whereas values above 100 make +# the output darker. The value divided by 100 is the actual gamma applied, +# so 80 represents a gamma of 0.8, The value 220 represents a gamma of 2.2, +# and 100 does not change the gamma. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML +# page will contain the date and time when the page was generated. Setting +# this to NO can help when comparing the output of multiple runs. + +HTML_TIMESTAMP = NO + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. + +HTML_DYNAMIC_SECTIONS = NO + +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of +# entries shown in the various tree structured indices initially; the user +# can expand and collapse entries dynamically later on. Doxygen will expand +# the tree to such a level that at most the specified number of entries are +# visible (unless a fully collapsed tree already exceeds this amount). +# So setting the number of entries 1 will produce a full collapsed tree by +# default. 0 is a special value representing an infinite number of entries +# and will result in a full expanded tree by default. + +HTML_INDEX_NUM_ENTRIES = 100 + +# If the GENERATE_DOCSET tag is set to YES, additional index files +# will be generated that can be used as input for Apple's Xcode 3 +# integrated development environment, introduced with OSX 10.5 (Leopard). +# To create a documentation set, doxygen will generate a Makefile in the +# HTML output directory. Running make will produce the docset in that +# directory and running "make install" will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find +# it at startup. +# See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html +# for more information. + +GENERATE_DOCSET = NO + +# When GENERATE_DOCSET tag is set to YES, this tag determines the name of the +# feed. A documentation feed provides an umbrella under which multiple +# documentation sets from a single provider (such as a company or product suite) +# can be grouped. + +DOCSET_FEEDNAME = "Doxygen generated docs" + +# When GENERATE_DOCSET tag is set to YES, this tag specifies a string that +# should uniquely identify the documentation set bundle. This should be a +# reverse domain-name style string, e.g. com.mycompany.MyDocSet. Doxygen +# will append .docset to the name. + +DOCSET_BUNDLE_ID = org.doxygen.Project + +# When GENERATE_PUBLISHER_ID tag specifies a string that should uniquely identify +# the documentation publisher. This should be a reverse domain-name style +# string, e.g. com.mycompany.MyDocSet.documentation. + +DOCSET_PUBLISHER_ID = org.doxygen.Publisher + +# The GENERATE_PUBLISHER_NAME tag identifies the documentation publisher. + +DOCSET_PUBLISHER_NAME = Publisher + +# If the GENERATE_HTMLHELP tag is set to YES, additional index files +# will be generated that can be used as input for tools like the +# Microsoft HTML help workshop to generate a compiled HTML help file (.chm) +# of the generated HTML documentation. + +GENERATE_HTMLHELP = NO + +# If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can +# be used to specify the file name of the resulting .chm file. You +# can add a path in front of the file if the result should not be +# written to the html output directory. + +CHM_FILE = + +# If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can +# be used to specify the location (absolute path including file name) of +# the HTML help compiler (hhc.exe). If non-empty doxygen will try to run +# the HTML help compiler on the generated index.hhp. + +HHC_LOCATION = + +# If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag +# controls if a separate .chi index file is generated (YES) or that +# it should be included in the master .chm file (NO). + +GENERATE_CHI = NO + +# If the GENERATE_HTMLHELP tag is set to YES, the CHM_INDEX_ENCODING +# is used to encode HtmlHelp index (hhk), content (hhc) and project file +# content. + +CHM_INDEX_ENCODING = + +# If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag +# controls whether a binary table of contents is generated (YES) or a +# normal table of contents (NO) in the .chm file. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members +# to the contents of the HTML help documentation and to the tree view. + +TOC_EXPAND = NO + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated +# that can be used as input for Qt's qhelpgenerator to generate a +# Qt Compressed Help (.qch) of the generated HTML documentation. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can +# be used to specify the file name of the resulting .qch file. +# The path specified is relative to the HTML output folder. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating +# Qt Help Project output. For more information please see +# http://doc.trolltech.com/qthelpproject.html#namespace + +QHP_NAMESPACE = org.doxygen.Project + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating +# Qt Help Project output. For more information please see +# http://doc.trolltech.com/qthelpproject.html#virtual-folders + +QHP_VIRTUAL_FOLDER = doc + +# If QHP_CUST_FILTER_NAME is set, it specifies the name of a custom filter to +# add. For more information please see +# http://doc.trolltech.com/qthelpproject.html#custom-filters + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILT_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see +# <a href="http://doc.trolltech.com/qthelpproject.html#custom-filters"> +# Qt Help Project / Custom Filters</a>. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's +# filter section matches. +# <a href="http://doc.trolltech.com/qthelpproject.html#filter-attributes"> +# Qt Help Project / Filter Attributes</a>. + +QHP_SECT_FILTER_ATTRS = + +# If the GENERATE_QHP tag is set to YES, the QHG_LOCATION tag can +# be used to specify the location of Qt's qhelpgenerator. +# If non-empty doxygen will try to run qhelpgenerator on the generated +# .qhp file. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files +# will be generated, which together with the HTML files, form an Eclipse help +# plugin. To install this plugin and make it available under the help contents +# menu in Eclipse, the contents of the directory containing the HTML and XML +# files needs to be copied into the plugins directory of eclipse. The name of +# the directory within the plugins directory should be the same as +# the ECLIPSE_DOC_ID value. After copying Eclipse needs to be restarted before +# the help appears. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have +# this name. + +ECLIPSE_DOC_ID = org.doxygen.Project + +# The DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) +# at top of each HTML page. The value NO (the default) enables the index and +# the value YES disables it. Since the tabs have the same information as the +# navigation tree you can set this option to NO if you already set +# GENERATE_TREEVIEW to YES. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. +# If the tag value is set to YES, a side panel will be generated +# containing a tree-like index structure (just like the one that +# is generated for HTML Help). For this to work a browser that supports +# JavaScript, DHTML, CSS and frames is required (i.e. any modern browser). +# Windows users are probably better off using the HTML help feature. +# Since the tree basically has the same information as the tab index you +# could consider to set DISABLE_INDEX to NO when enabling this option. + +GENERATE_TREEVIEW = NO + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values +# (range [0,1..20]) that doxygen will group on one line in the generated HTML +# documentation. Note that a value of 0 will completely suppress the enum +# values from appearing in the overview section. + +ENUM_VALUES_PER_LINE = 1 + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be +# used to set the initial width (in pixels) of the frame in which the tree +# is shown. + +TREEVIEW_WIDTH = 250 + +# When the EXT_LINKS_IN_WINDOW option is set to YES doxygen will open +# links to external symbols imported via tag files in a separate window. + +EXT_LINKS_IN_WINDOW = NO + +# Use this tag to change the font size of Latex formulas included +# as images in the HTML documentation. The default is 10. Note that +# when you change the font size after a successful doxygen run you need +# to manually remove any form_*.png images from the HTML output directory +# to force them to be regenerated. + +FORMULA_FONTSIZE = 10 + +# Use the FORMULA_TRANPARENT tag to determine whether or not the images +# generated for formulas are transparent PNGs. Transparent PNGs are +# not supported properly for IE 6.0, but are supported on all modern browsers. +# Note that when changing this option you need to delete any form_*.png files +# in the HTML output before the changes have effect. + +FORMULA_TRANSPARENT = YES + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax +# (see http://www.mathjax.org) which uses client side Javascript for the +# rendering instead of using prerendered bitmaps. Use this if you do not +# have LaTeX installed or if you want to formulas look prettier in the HTML +# output. When enabled you may also need to install MathJax separately and +# configure the path to it using the MATHJAX_RELPATH option. + +USE_MATHJAX = NO + +# When MathJax is enabled you need to specify the location relative to the +# HTML output directory using the MATHJAX_RELPATH option. The destination +# directory should contain the MathJax.js script. For instance, if the mathjax +# directory is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to +# the MathJax Content Delivery Network so you can quickly see the result without +# installing MathJax. +# However, it is strongly recommended to install a local +# copy of MathJax from http://www.mathjax.org before deployment. + +MATHJAX_RELPATH = http://www.mathjax.org/mathjax + +# The MATHJAX_EXTENSIONS tag can be used to specify one or MathJax extension +# names that should be enabled during MathJax rendering. + +MATHJAX_EXTENSIONS = + +# When the SEARCHENGINE tag is enabled doxygen will generate a search box +# for the HTML output. The underlying search engine uses javascript +# and DHTML and should work on any modern browser. Note that when using +# HTML help (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets +# (GENERATE_DOCSET) there is already a search function so this one should +# typically be disabled. For large projects the javascript based search engine +# can be slow, then enabling SERVER_BASED_SEARCH may provide a better solution. + +SEARCHENGINE = NO + +# When the SERVER_BASED_SEARCH tag is enabled the search engine will be +# implemented using a PHP enabled web server instead of at the web client +# using Javascript. Doxygen will generate the search PHP script and index +# file to put on the web server. The advantage of the server +# based approach is that it scales better to large projects and allows +# full text search. The disadvantages are that it is more difficult to setup +# and does not have live searching capabilities. + +SERVER_BASED_SEARCH = NO + +#--------------------------------------------------------------------------- +# configuration options related to the LaTeX output +#--------------------------------------------------------------------------- + +# If the GENERATE_LATEX tag is set to YES (the default) Doxygen will +# generate Latex output. + +GENERATE_LATEX = NO + +# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `latex' will be used as the default path. + +LATEX_OUTPUT = latex + +# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be +# invoked. If left blank `latex' will be used as the default command name. +# Note that when enabling USE_PDFLATEX this option is only used for +# generating bitmaps for formulas in the HTML output, but not in the +# Makefile that is written to the output directory. + +LATEX_CMD_NAME = latex + +# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to +# generate index for LaTeX. If left blank `makeindex' will be used as the +# default command name. + +MAKEINDEX_CMD_NAME = makeindex + +# If the COMPACT_LATEX tag is set to YES Doxygen generates more compact +# LaTeX documents. This may be useful for small projects and may help to +# save some trees in general. + +COMPACT_LATEX = NO + +# The PAPER_TYPE tag can be used to set the paper type that is used +# by the printer. Possible values are: a4, letter, legal and +# executive. If left blank a4wide will be used. + +PAPER_TYPE = a4 + +# The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX +# packages that should be included in the LaTeX output. + +EXTRA_PACKAGES = + +# The LATEX_HEADER tag can be used to specify a personal LaTeX header for +# the generated latex document. The header should contain everything until +# the first chapter. If it is left blank doxygen will generate a +# standard header. Notice: only use this tag if you know what you are doing! + +LATEX_HEADER = + +# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for +# the generated latex document. The footer should contain everything after +# the last chapter. If it is left blank doxygen will generate a +# standard footer. Notice: only use this tag if you know what you are doing! + +LATEX_FOOTER = + +# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated +# is prepared for conversion to pdf (using ps2pdf). The pdf file will +# contain links (just like the HTML output) instead of page references +# This makes the output suitable for online browsing using a pdf viewer. + +PDF_HYPERLINKS = YES + +# If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of +# plain latex in the generated Makefile. Set this option to YES to get a +# higher quality PDF documentation. + +USE_PDFLATEX = YES + +# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode. +# command to the generated LaTeX files. This will instruct LaTeX to keep +# running if errors occur, instead of asking the user for help. +# This option is also used when generating formulas in HTML. + +LATEX_BATCHMODE = NO + +# If LATEX_HIDE_INDICES is set to YES then doxygen will not +# include the index chapters (such as File Index, Compound Index, etc.) +# in the output. + +LATEX_HIDE_INDICES = NO + +# If LATEX_SOURCE_CODE is set to YES then doxygen will include +# source code with syntax highlighting in the LaTeX output. +# Note that which sources are shown also depends on other settings +# such as SOURCE_BROWSER. + +LATEX_SOURCE_CODE = NO + +# The LATEX_BIB_STYLE tag can be used to specify the style to use for the +# bibliography, e.g. plainnat, or ieeetr. The default style is "plain". See +# http://en.wikipedia.org/wiki/BibTeX for more info. + +LATEX_BIB_STYLE = plain + +#--------------------------------------------------------------------------- +# configuration options related to the RTF output +#--------------------------------------------------------------------------- + +# If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output +# The RTF output is optimized for Word 97 and may not look very pretty with +# other RTF readers or editors. + +GENERATE_RTF = NO + +# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `rtf' will be used as the default path. + +RTF_OUTPUT = rtf + +# If the COMPACT_RTF tag is set to YES Doxygen generates more compact +# RTF documents. This may be useful for small projects and may help to +# save some trees in general. + +COMPACT_RTF = NO + +# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated +# will contain hyperlink fields. The RTF file will +# contain links (just like the HTML output) instead of page references. +# This makes the output suitable for online browsing using WORD or other +# programs which support those fields. +# Note: wordpad (write) and others do not support links. + +RTF_HYPERLINKS = NO + +# Load style sheet definitions from file. Syntax is similar to doxygen's +# config file, i.e. a series of assignments. You only have to provide +# replacements, missing definitions are set to their default value. + +RTF_STYLESHEET_FILE = + +# Set optional variables used in the generation of an rtf document. +# Syntax is similar to doxygen's config file. + +RTF_EXTENSIONS_FILE = + +#--------------------------------------------------------------------------- +# configuration options related to the man page output +#--------------------------------------------------------------------------- + +# If the GENERATE_MAN tag is set to YES (the default) Doxygen will +# generate man pages + +GENERATE_MAN = NO + +# The MAN_OUTPUT tag is used to specify where the man pages will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `man' will be used as the default path. + +MAN_OUTPUT = man + +# The MAN_EXTENSION tag determines the extension that is added to +# the generated man pages (default is the subroutine's section .3) + +MAN_EXTENSION = .3 + +# If the MAN_LINKS tag is set to YES and Doxygen generates man output, +# then it will generate one additional man file for each entity +# documented in the real man page(s). These additional files +# only source the real man page, but without them the man command +# would be unable to find the correct page. The default is NO. + +MAN_LINKS = NO + +#--------------------------------------------------------------------------- +# configuration options related to the XML output +#--------------------------------------------------------------------------- + +# If the GENERATE_XML tag is set to YES Doxygen will +# generate an XML file that captures the structure of +# the code including all documentation. + +GENERATE_XML = NO + +# The XML_OUTPUT tag is used to specify where the XML pages will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `xml' will be used as the default path. + +XML_OUTPUT = xml + +# The XML_SCHEMA tag can be used to specify an XML schema, +# which can be used by a validating XML parser to check the +# syntax of the XML files. + +XML_SCHEMA = + +# The XML_DTD tag can be used to specify an XML DTD, +# which can be used by a validating XML parser to check the +# syntax of the XML files. + +XML_DTD = + +# If the XML_PROGRAMLISTING tag is set to YES Doxygen will +# dump the program listings (including syntax highlighting +# and cross-referencing information) to the XML output. Note that +# enabling this will significantly increase the size of the XML output. + +XML_PROGRAMLISTING = YES + +#--------------------------------------------------------------------------- +# configuration options for the AutoGen Definitions output +#--------------------------------------------------------------------------- + +# If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will +# generate an AutoGen Definitions (see autogen.sf.net) file +# that captures the structure of the code including all +# documentation. Note that this feature is still experimental +# and incomplete at the moment. + +GENERATE_AUTOGEN_DEF = NO + +#--------------------------------------------------------------------------- +# configuration options related to the Perl module output +#--------------------------------------------------------------------------- + +# If the GENERATE_PERLMOD tag is set to YES Doxygen will +# generate a Perl module file that captures the structure of +# the code including all documentation. Note that this +# feature is still experimental and incomplete at the +# moment. + +GENERATE_PERLMOD = NO + +# If the PERLMOD_LATEX tag is set to YES Doxygen will generate +# the necessary Makefile rules, Perl scripts and LaTeX code to be able +# to generate PDF and DVI output from the Perl module output. + +PERLMOD_LATEX = NO + +# If the PERLMOD_PRETTY tag is set to YES the Perl module output will be +# nicely formatted so it can be parsed by a human reader. +# This is useful +# if you want to understand what is going on. +# On the other hand, if this +# tag is set to NO the size of the Perl module output will be much smaller +# and Perl will parse it just the same. + +PERLMOD_PRETTY = YES + +# The names of the make variables in the generated doxyrules.make file +# are prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. +# This is useful so different doxyrules.make files included by the same +# Makefile don't overwrite each other's variables. + +PERLMOD_MAKEVAR_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the preprocessor +#--------------------------------------------------------------------------- + +# If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will +# evaluate all C-preprocessor directives found in the sources and include +# files. + +ENABLE_PREPROCESSING = YES + +# If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro +# names in the source code. If set to NO (the default) only conditional +# compilation will be performed. Macro expansion can be done in a controlled +# way by setting EXPAND_ONLY_PREDEF to YES. + +MACRO_EXPANSION = YES + +# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES +# then the macro expansion is limited to the macros specified with the +# PREDEFINED and EXPAND_AS_DEFINED tags. + +EXPAND_ONLY_PREDEF = YES + +# If the SEARCH_INCLUDES tag is set to YES (the default) the includes files +# pointed to by INCLUDE_PATH will be searched when a #include is found. + +SEARCH_INCLUDES = YES + +# The INCLUDE_PATH tag can be used to specify one or more directories that +# contain include files that are not input files but should be processed by +# the preprocessor. + +INCLUDE_PATH = + +# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard +# patterns (like *.h and *.hpp) to filter out the header-files in the +# directories. If left blank, the patterns specified with FILE_PATTERNS will +# be used. + +INCLUDE_FILE_PATTERNS = + +# The PREDEFINED tag can be used to specify one or more macro names that +# are defined before the preprocessor is started (similar to the -D option of +# gcc). The argument of the tag is a list of macros of the form: name +# or name=definition (no spaces). If the definition and the = are +# omitted =1 is assumed. To prevent a macro definition from being +# undefined via #undef or recursively expanded use the := operator +# instead of the = operator. + +PREDEFINED = ORTHANC_PLUGIN_INLINE= + +# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then +# this tag can be used to specify a list of macro names that should be expanded. +# The macro definition that is found in the sources will be used. +# Use the PREDEFINED tag if you want to use a different macro definition that +# overrules the definition found in the source code. + +EXPAND_AS_DEFINED = + +# If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then +# doxygen's preprocessor will remove all references to function-like macros +# that are alone on a line, have an all uppercase name, and do not end with a +# semicolon, because these will confuse the parser if not removed. + +SKIP_FUNCTION_MACROS = YES + +#--------------------------------------------------------------------------- +# Configuration::additions related to external references +#--------------------------------------------------------------------------- + +# The TAGFILES option can be used to specify one or more tagfiles. For each +# tag file the location of the external documentation should be added. The +# format of a tag file without this location is as follows: +# +# TAGFILES = file1 file2 ... +# Adding location for the tag files is done as follows: +# +# TAGFILES = file1=loc1 "file2 = loc2" ... +# where "loc1" and "loc2" can be relative or absolute paths +# or URLs. Note that each tag file must have a unique name (where the name does +# NOT include the path). If a tag file is not located in the directory in which +# doxygen is run, you must also specify the path to the tagfile here. + +TAGFILES = + +# When a file name is specified after GENERATE_TAGFILE, doxygen will create +# a tag file that is based on the input files it reads. + +GENERATE_TAGFILE = + +# If the ALLEXTERNALS tag is set to YES all external classes will be listed +# in the class index. If set to NO only the inherited external classes +# will be listed. + +ALLEXTERNALS = NO + +# If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed +# in the modules index. If set to NO, only the current project's groups will +# be listed. + +EXTERNAL_GROUPS = YES + +# The PERL_PATH should be the absolute path and name of the perl script +# interpreter (i.e. the result of `which perl'). + +PERL_PATH = /usr/bin/perl + +#--------------------------------------------------------------------------- +# Configuration options related to the dot tool +#--------------------------------------------------------------------------- + +# If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will +# generate a inheritance diagram (in HTML, RTF and LaTeX) for classes with base +# or super classes. Setting the tag to NO turns the diagrams off. Note that +# this option also works with HAVE_DOT disabled, but it is recommended to +# install and use dot, since it yields more powerful graphs. + +CLASS_DIAGRAMS = YES + +# You can define message sequence charts within doxygen comments using the \msc +# command. Doxygen will then run the mscgen tool (see +# http://www.mcternan.me.uk/mscgen/) to produce the chart and insert it in the +# documentation. The MSCGEN_PATH tag allows you to specify the directory where +# the mscgen tool resides. If left empty the tool is assumed to be found in the +# default search path. + +MSCGEN_PATH = + +# If set to YES, the inheritance and collaboration graphs will hide +# inheritance and usage relations if the target is undocumented +# or is not a class. + +HIDE_UNDOC_RELATIONS = YES + +# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is +# available from the path. This tool is part of Graphviz, a graph visualization +# toolkit from AT&T and Lucent Bell Labs. The other options in this section +# have no effect if this option is set to NO (the default) + +HAVE_DOT = NO + +# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is +# allowed to run in parallel. When set to 0 (the default) doxygen will +# base this on the number of processors available in the system. You can set it +# explicitly to a value larger than 0 to get control over the balance +# between CPU load and processing speed. + +DOT_NUM_THREADS = 0 + +# By default doxygen will use the Helvetica font for all dot files that +# doxygen generates. When you want a differently looking font you can specify +# the font name using DOT_FONTNAME. You need to make sure dot is able to find +# the font, which can be done by putting it in a standard location or by setting +# the DOTFONTPATH environment variable or by setting DOT_FONTPATH to the +# directory containing the font. + +DOT_FONTNAME = Helvetica + +# The DOT_FONTSIZE tag can be used to set the size of the font of dot graphs. +# The default size is 10pt. + +DOT_FONTSIZE = 10 + +# By default doxygen will tell dot to use the Helvetica font. +# If you specify a different font using DOT_FONTNAME you can use DOT_FONTPATH to +# set the path where dot can find it. + +DOT_FONTPATH = + +# If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for each documented class showing the direct and +# indirect inheritance relations. Setting this tag to YES will force the +# CLASS_DIAGRAMS tag to NO. + +CLASS_GRAPH = YES + +# If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for each documented class showing the direct and +# indirect implementation dependencies (inheritance, containment, and +# class references variables) of the class with other documented classes. + +COLLABORATION_GRAPH = YES + +# If the GROUP_GRAPHS and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for groups, showing the direct groups dependencies + +GROUP_GRAPHS = YES + +# If the UML_LOOK tag is set to YES doxygen will generate inheritance and +# collaboration diagrams in a style similar to the OMG's Unified Modeling +# Language. + +UML_LOOK = NO + +# If the UML_LOOK tag is enabled, the fields and methods are shown inside +# the class node. If there are many fields or methods and many nodes the +# graph may become too big to be useful. The UML_LIMIT_NUM_FIELDS +# threshold limits the number of items for each type to make the size more +# managable. Set this to 0 for no limit. Note that the threshold may be +# exceeded by 50% before the limit is enforced. + +UML_LIMIT_NUM_FIELDS = 10 + +# If set to YES, the inheritance and collaboration graphs will show the +# relations between templates and their instances. + +TEMPLATE_RELATIONS = NO + +# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT +# tags are set to YES then doxygen will generate a graph for each documented +# file showing the direct and indirect include dependencies of the file with +# other documented files. + +INCLUDE_GRAPH = YES + +# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and +# HAVE_DOT tags are set to YES then doxygen will generate a graph for each +# documented header file showing the documented files that directly or +# indirectly include this file. + +INCLUDED_BY_GRAPH = YES + +# If the CALL_GRAPH and HAVE_DOT options are set to YES then +# doxygen will generate a call dependency graph for every global function +# or class method. Note that enabling this option will significantly increase +# the time of a run. So in most cases it will be better to enable call graphs +# for selected functions only using the \callgraph command. + +CALL_GRAPH = NO + +# If the CALLER_GRAPH and HAVE_DOT tags are set to YES then +# doxygen will generate a caller dependency graph for every global function +# or class method. Note that enabling this option will significantly increase +# the time of a run. So in most cases it will be better to enable caller +# graphs for selected functions only using the \callergraph command. + +CALLER_GRAPH = NO + +# If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen +# will generate a graphical hierarchy of all classes instead of a textual one. + +GRAPHICAL_HIERARCHY = YES + +# If the DIRECTORY_GRAPH and HAVE_DOT tags are set to YES +# then doxygen will show the dependencies a directory has on other directories +# in a graphical way. The dependency relations are determined by the #include +# relations between the files in the directories. + +DIRECTORY_GRAPH = YES + +# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images +# generated by dot. Possible values are svg, png, jpg, or gif. +# If left blank png will be used. If you choose svg you need to set +# HTML_FILE_EXTENSION to xhtml in order to make the SVG files +# visible in IE 9+ (other browsers do not have this requirement). + +DOT_IMAGE_FORMAT = png + +# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to +# enable generation of interactive SVG images that allow zooming and panning. +# Note that this requires a modern browser other than Internet Explorer. +# Tested and working are Firefox, Chrome, Safari, and Opera. For IE 9+ you +# need to set HTML_FILE_EXTENSION to xhtml in order to make the SVG files +# visible. Older versions of IE do not have SVG support. + +INTERACTIVE_SVG = NO + +# The tag DOT_PATH can be used to specify the path where the dot tool can be +# found. If left blank, it is assumed the dot tool can be found in the path. + +DOT_PATH = + +# The DOTFILE_DIRS tag can be used to specify one or more directories that +# contain dot files that are included in the documentation (see the +# \dotfile command). + +DOTFILE_DIRS = + +# The MSCFILE_DIRS tag can be used to specify one or more directories that +# contain msc files that are included in the documentation (see the +# \mscfile command). + +MSCFILE_DIRS = + +# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of +# nodes that will be shown in the graph. If the number of nodes in a graph +# becomes larger than this value, doxygen will truncate the graph, which is +# visualized by representing a node as a red box. Note that doxygen if the +# number of direct children of the root node in a graph is already larger than +# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note +# that the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH. + +DOT_GRAPH_MAX_NODES = 50 + +# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the +# graphs generated by dot. A depth value of 3 means that only nodes reachable +# from the root by following a path via at most 3 edges will be shown. Nodes +# that lay further from the root node will be omitted. Note that setting this +# option to 1 or 2 may greatly reduce the computation time needed for large +# code bases. Also note that the size of a graph can be further restricted by +# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction. + +MAX_DOT_GRAPH_DEPTH = 0 + +# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent +# background. This is disabled by default, because dot on Windows does not +# seem to support this out of the box. Warning: Depending on the platform used, +# enabling this option may lead to badly anti-aliased labels on the edges of +# a graph (i.e. they become hard to read). + +DOT_TRANSPARENT = NO + +# Set the DOT_MULTI_TARGETS tag to YES allow dot to generate multiple output +# files in one run (i.e. multiple -o and -T options on the command line). This +# makes dot run faster, but since only newer versions of dot (>1.8.10) +# support this, this feature is disabled by default. + +DOT_MULTI_TARGETS = YES + +# If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will +# generate a legend page explaining the meaning of the various boxes and +# arrows in the dot generated graphs. + +GENERATE_LEGEND = YES + +# If the DOT_CLEANUP tag is set to YES (the default) Doxygen will +# remove the intermediate dot files that are used to generate +# the various graphs. + +DOT_CLEANUP = YES
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Patches/boost-1.55.0-clang-atomic.patch Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,87 @@ +--- boost/atomic/detail/cas128strong.hpp ++++ boost/atomic/detail/cas128strong.hpp +@@ -196,15 +196,17 @@ class base_atomic<T, void, 16, Sign> + + public: + BOOST_DEFAULTED_FUNCTION(base_atomic(void), {}) +- explicit base_atomic(value_type const& v) BOOST_NOEXCEPT : v_(0) ++ explicit base_atomic(value_type const& v) BOOST_NOEXCEPT + { ++ memset(&v_, 0, sizeof(v_)); + memcpy(&v_, &v, sizeof(value_type)); + } + + void + store(value_type const& value, memory_order order = memory_order_seq_cst) volatile BOOST_NOEXCEPT + { +- storage_type value_s = 0; ++ storage_type value_s; ++ memset(&value_s, 0, sizeof(value_s)); + memcpy(&value_s, &value, sizeof(value_type)); + platform_fence_before_store(order); + platform_store128(value_s, &v_); +@@ -247,7 +249,9 @@ class base_atomic<T, void, 16, Sign> + memory_order success_order, + memory_order failure_order) volatile BOOST_NOEXCEPT + { +- storage_type expected_s = 0, desired_s = 0; ++ storage_type expected_s, desired_s; ++ memset(&expected_s, 0, sizeof(expected_s)); ++ memset(&desired_s, 0, sizeof(desired_s)); + memcpy(&expected_s, &expected, sizeof(value_type)); + memcpy(&desired_s, &desired, sizeof(value_type)); + +--- boost/atomic/detail/gcc-atomic.hpp ++++ boost/atomic/detail/gcc-atomic.hpp +@@ -958,14 +958,16 @@ class base_atomic<T, void, 16, Sign> + + public: + BOOST_DEFAULTED_FUNCTION(base_atomic(void), {}) +- explicit base_atomic(value_type const& v) BOOST_NOEXCEPT : v_(0) ++ explicit base_atomic(value_type const& v) BOOST_NOEXCEPT + { ++ memset(&v_, 0, sizeof(v_)); + memcpy(&v_, &v, sizeof(value_type)); + } + + void store(value_type const& v, memory_order order = memory_order_seq_cst) volatile BOOST_NOEXCEPT + { +- storage_type tmp = 0; ++ storage_type tmp; ++ memset(&tmp, 0, sizeof(tmp)); + memcpy(&tmp, &v, sizeof(value_type)); + __atomic_store_n(&v_, tmp, atomics::detail::convert_memory_order_to_gcc(order)); + } +@@ -980,7 +982,8 @@ class base_atomic<T, void, 16, Sign> + + value_type exchange(value_type const& v, memory_order order = memory_order_seq_cst) volatile BOOST_NOEXCEPT + { +- storage_type tmp = 0; ++ storage_type tmp; ++ memset(&tmp, 0, sizeof(tmp)); + memcpy(&tmp, &v, sizeof(value_type)); + tmp = __atomic_exchange_n(&v_, tmp, atomics::detail::convert_memory_order_to_gcc(order)); + value_type res; +@@ -994,7 +997,9 @@ class base_atomic<T, void, 16, Sign> + memory_order success_order, + memory_order failure_order) volatile BOOST_NOEXCEPT + { +- storage_type expected_s = 0, desired_s = 0; ++ storage_type expected_s, desired_s; ++ memset(&expected_s, 0, sizeof(expected_s)); ++ memset(&desired_s, 0, sizeof(desired_s)); + memcpy(&expected_s, &expected, sizeof(value_type)); + memcpy(&desired_s, &desired, sizeof(value_type)); + const bool success = __atomic_compare_exchange_n(&v_, &expected_s, desired_s, false, +@@ -1010,7 +1015,9 @@ class base_atomic<T, void, 16, Sign> + memory_order success_order, + memory_order failure_order) volatile BOOST_NOEXCEPT + { +- storage_type expected_s = 0, desired_s = 0; ++ storage_type expected_s, desired_s; ++ memset(&expected_s, 0, sizeof(expected_s)); ++ memset(&desired_s, 0, sizeof(desired_s)); + memcpy(&expected_s, &expected, sizeof(value_type)); + memcpy(&desired_s, &desired, sizeof(value_type)); + const bool success = __atomic_compare_exchange_n(&v_, &expected_s, desired_s, true, +--
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Patches/dcmtk-linux-speed.patch Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,24 @@ +diff -urEb dcmtk-3.6.0.orig/dcmnet/libsrc/dul.cc dcmtk-3.6.0/dcmnet/libsrc/dul.cc +--- dcmtk-3.6.0.orig/dcmnet/libsrc/dul.cc 2010-12-01 09:26:36.000000000 +0100 ++++ dcmtk-3.6.0/dcmnet/libsrc/dul.cc 2015-05-15 17:03:50.762451757 +0200 +@@ -1840,7 +1840,7 @@ + } + #endif + #endif +- setTCPBufferLength(sock); ++ //setTCPBufferLength(sock); + + #ifndef DONT_DISABLE_NAGLE_ALGORITHM + /* +diff -urEb dcmtk-3.6.0.orig/dcmnet/libsrc/dulfsm.cc dcmtk-3.6.0/dcmnet/libsrc/dulfsm.cc +--- dcmtk-3.6.0.orig/dcmnet/libsrc/dulfsm.cc 2010-12-01 09:26:36.000000000 +0100 ++++ dcmtk-3.6.0/dcmnet/libsrc/dulfsm.cc 2015-05-15 17:03:55.570451952 +0200 +@@ -2417,7 +2417,7 @@ + return makeDcmnetCondition(DULC_TCPINITERROR, OF_error, msg.c_str()); + } + #endif +- setTCPBufferLength(s); ++ //setTCPBufferLength(s); + + #ifndef DONT_DISABLE_NAGLE_ALGORITHM + /*
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Patches/glog-visual-studio-port.h Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,178 @@ +/* Copyright (c) 2008, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * --- + * Author: Craig Silverstein + * Copied from google-perftools and modified by Shinichiro Hamaji + * + * These are some portability typedefs and defines to make it a bit + * easier to compile this code under VC++. + * + * Several of these are taken from glib: + * http://developer.gnome.org/doc/API/glib/glib-windows-compatability-functions.html + */ + +#ifndef CTEMPLATE_WINDOWS_PORT_H_ +#define CTEMPLATE_WINDOWS_PORT_H_ + +#include "config.h" + +#ifdef _WIN32 + +#define WIN32_LEAN_AND_MEAN /* We always want minimal includes */ +#include <windows.h> +#include <winsock.h> /* for gethostname */ +#include <io.h> /* because we so often use open/close/etc */ +#include <direct.h> /* for _getcwd() */ +#include <process.h> /* for _getpid() */ +#include <stdio.h> /* read in vsnprintf decl. before redifining it */ +#include <stdarg.h> /* template_dictionary.cc uses va_copy */ +#include <string.h> /* for _strnicmp(), strerror_s() */ +#include <time.h> /* for localtime_s() */ +/* Note: the C++ #includes are all together at the bottom. This file is + * used by both C and C++ code, so we put all the C++ together. + */ + +// Fix by Sebastien Jodogne for Visual Studio 2013 +// https://code.google.com/p/google-glog/issues/detail?id=212 +#if defined(_MSC_VER) && (_MSC_VER >= 1800) +#include <algorithm> +#endif + +/* 4244: otherwise we get problems when substracting two size_t's to an int + * 4251: it's complaining about a private struct I've chosen not to dllexport + * 4355: we use this in a constructor, but we do it safely + * 4715: for some reason VC++ stopped realizing you can't return after abort() + * 4800: we know we're casting ints/char*'s to bools, and we're ok with that + * 4996: Yes, we're ok using "unsafe" functions like fopen() and strerror() + */ +#pragma warning(disable:4244 4251 4355 4715 4800 4996) + +/* file I/O */ +#define PATH_MAX 1024 +#define access _access +#define getcwd _getcwd +#define open _open +#define read _read +#define write _write +#define lseek _lseek +#define close _close +#define popen _popen +#define pclose _pclose +#define R_OK 04 /* read-only (for access()) */ +#define S_ISDIR(m) (((m) & _S_IFMT) == _S_IFDIR) +#ifndef __MINGW32__ +enum { STDIN_FILENO = 0, STDOUT_FILENO = 1, STDERR_FILENO = 2 }; +#endif +#define S_IRUSR S_IREAD +#define S_IWUSR S_IWRITE + +/* Not quite as lightweight as a hard-link, but more than good enough for us. */ +#define link(oldpath, newpath) CopyFileA(oldpath, newpath, false) + +#define strcasecmp _stricmp +#define strncasecmp _strnicmp + +/* In windows-land, hash<> is called hash_compare<> (from xhash.h) */ +#if _MSC_VER < 1700 +#define hash hash_compare +#endif + +/* Sleep is in ms, on windows */ +#define sleep(secs) Sleep((secs) * 1000) + +/* We can't just use _vsnprintf and _snprintf as drop-in-replacements, + * because they don't always NUL-terminate. :-( We also can't use the + * name vsnprintf, since windows defines that (but not snprintf (!)). + */ +extern int snprintf(char *str, size_t size, + const char *format, ...); +extern int safe_vsnprintf(char *str, size_t size, + const char *format, va_list ap); +#define vsnprintf(str, size, format, ap) safe_vsnprintf(str, size, format, ap) +#define va_copy(dst, src) (dst) = (src) + +/* Windows doesn't support specifying the number of buckets as a + * hash_map constructor arg, so we leave this blank. + */ +#define CTEMPLATE_SMALL_HASHTABLE + +#define DEFAULT_TEMPLATE_ROOTDIR ".." + +// ----------------------------------- SYSTEM/PROCESS +typedef int pid_t; +#define getpid _getpid + +// ----------------------------------- THREADS +typedef DWORD pthread_t; +typedef DWORD pthread_key_t; +typedef LONG pthread_once_t; +enum { PTHREAD_ONCE_INIT = 0 }; // important that this be 0! for SpinLock +#define pthread_self GetCurrentThreadId +#define pthread_equal(pthread_t_1, pthread_t_2) ((pthread_t_1)==(pthread_t_2)) + +#if defined(__MINGW32__) +inline int localtime_s(tm * _tm, const time_t * time) +{ + tm * posix_local_time_struct = localtime(time); + if (posix_local_time_struct == NULL) + { + return 1; + } + + *_tm = *posix_local_time_struct; + + return 0; +} + +inline char* strerror_s(char* buf, size_t buflen, int errnum) +{ + const char* str = strerror(errnum); + return strncpy(buf, str, buflen - 1); +} +#endif + +inline struct tm* localtime_r(const time_t* timep, struct tm* result) { + localtime_s(result, timep); + return result; +} + +inline char* strerror_r(int errnum, char* buf, size_t buflen) { + strerror_s(buf, buflen, errnum); + return buf; +} + +#ifndef __cplusplus +/* I don't see how to get inlining for C code in MSVC. Ah well. */ +#define inline +#endif + +#endif /* _WIN32 */ + +#endif /* CTEMPLATE_WINDOWS_PORT_H_ */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Patches/mongoose-3.1-patch.diff Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,54 @@ +--- /home/jodogne/Subversion/Orthanc/ThirdPartyDownloads/mongoose/mongoose.c 2012-03-11 23:41:35.000000000 +0100 ++++ mongoose.c 2013-03-07 10:07:00.566266153 +0100 +@@ -92,8 +92,9 @@ + #define strtoll(x, y, z) strtol(x, y, z) + #else + #define __func__ __FUNCTION__ +-#define strtoull(x, y, z) _strtoui64(x, y, z) +-#define strtoll(x, y, z) _strtoi64(x, y, z) ++#include <stdlib.h> ++//#define strtoull(x, y, z) _strtoui64(x, y, z) ++//#define strtoll(x, y, z) _strtoi64(x, y, z) + #endif // _MSC_VER + + #define ERRNO GetLastError() +@@ -253,6 +254,14 @@ + #define MSG_NOSIGNAL 0 + #endif + ++#if __gnu_hurd__ == 1 ++/** ++ * There is no limit on the length on a path under GNU Hurd, so we set ++ * it to an arbitrary constant. ++ **/ ++#define PATH_MAX 4096 ++#endif ++ + typedef void * (*mg_thread_func_t)(void *); + + static const char *http_500_error = "Internal Server Error"; +@@ -3844,10 +3853,8 @@ + } + + static void discard_current_request_from_buffer(struct mg_connection *conn) { +- char *buffered; + int buffered_len, body_len; + +- buffered = conn->buf + conn->request_len; + buffered_len = conn->data_len - conn->request_len; + assert(buffered_len >= 0); + +@@ -4148,7 +4155,13 @@ + + // Wait until mg_fini() stops + while (ctx->stop_flag != 2) { ++#if defined(__linux) ++ usleep(100000); ++#elif defined(_WIN32) ++ Sleep(100); ++#else + (void) sleep(0); ++#endif + } + free_context(ctx); +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Patches/mongoose-3.8-patch.diff Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,29 @@ +--- mongoose.c.orig 2014-09-01 11:25:18.223466994 +0200 ++++ mongoose.c 2014-09-01 11:30:21.807479338 +0200 +@@ -50,6 +50,14 @@ + #define PATH_MAX FILENAME_MAX + #endif // __SYMBIAN32__ + ++#if __gnu_hurd__ == 1 ++/** ++ * There is no limit on the length on a path under GNU Hurd, so we set ++ * it to an arbitrary constant. ++ **/ ++#define PATH_MAX 4096 ++#endif ++ + #ifndef _WIN32_WCE // Some ANSI #includes are not available on Windows CE + #include <sys/types.h> + #include <sys/stat.h> +@@ -108,8 +116,9 @@ + #define strtoll(x, y, z) _atoi64(x) + #else + #define __func__ __FUNCTION__ +-#define strtoull(x, y, z) _strtoui64(x, y, z) +-#define strtoll(x, y, z) _strtoi64(x, y, z) ++#include <stdlib.h> ++//#define strtoull(x, y, z) _strtoui64(x, y, z) ++//#define strtoll(x, y, z) _strtoi64(x, y, z) + #endif // _MSC_VER + + #define ERRNO GetLastError()
--- a/Resources/Patches/mongoose-patch.diff Wed Jun 25 15:34:40 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,54 +0,0 @@ ---- /home/jodogne/Subversion/Orthanc/ThirdPartyDownloads/mongoose/mongoose.c 2012-03-11 23:41:35.000000000 +0100 -+++ mongoose.c 2013-03-07 10:07:00.566266153 +0100 -@@ -92,8 +92,9 @@ - #define strtoll(x, y, z) strtol(x, y, z) - #else - #define __func__ __FUNCTION__ --#define strtoull(x, y, z) _strtoui64(x, y, z) --#define strtoll(x, y, z) _strtoi64(x, y, z) -+#include <stdlib.h> -+//#define strtoull(x, y, z) _strtoui64(x, y, z) -+//#define strtoll(x, y, z) _strtoi64(x, y, z) - #endif // _MSC_VER - - #define ERRNO GetLastError() -@@ -253,6 +254,14 @@ - #define MSG_NOSIGNAL 0 - #endif - -+#if __gnu_hurd__ == 1 -+/** -+ * There is no limit on the length on a path under GNU Hurd, so we set -+ * it to an arbitrary constant. -+ **/ -+#define PATH_MAX 4096 -+#endif -+ - typedef void * (*mg_thread_func_t)(void *); - - static const char *http_500_error = "Internal Server Error"; -@@ -3844,10 +3853,8 @@ - } - - static void discard_current_request_from_buffer(struct mg_connection *conn) { -- char *buffered; - int buffered_len, body_len; - -- buffered = conn->buf + conn->request_len; - buffered_len = conn->data_len - conn->request_len; - assert(buffered_len >= 0); - -@@ -4148,7 +4155,13 @@ - - // Wait until mg_fini() stops - while (ctx->stop_flag != 2) { -+#if defined(__linux) -+ usleep(100000); -+#elif defined(_WIN32) -+ Sleep(100); -+#else - (void) sleep(0); -+#endif - } - free_context(ctx); -
--- a/Resources/Samples/ImportDicomFiles/ImportDicomFiles.py Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/Samples/ImportDicomFiles/ImportDicomFiles.py Thu May 21 16:58:30 2015 +0200 @@ -1,8 +1,8 @@ #!/usr/bin/python # Orthanc - A Lightweight, RESTful DICOM Store -# Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, -# Belgium +# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics +# Department, University Hospital 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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Samples/Lua/Autorouting.lua Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,3 @@ +function OnStoredInstance(instanceId, tags, metadata) + Delete(SendToModality(instanceId, 'sample')) +end
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Samples/Lua/AutoroutingConditional.lua Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,19 @@ +function OnStoredInstance(instanceId, tags, metadata, remoteAet, calledAet) + -- The "remoteAet" and "calledAet" arguments are only available + -- since Orthanc 0.8.6 + if remoteAet ~=nil and calledAet ~= nil then + print ("Source AET: " .. remoteAet .. " => Called AET: " .. calledAet) + end + + -- Extract the value of the "PatientName" DICOM tag + local patientName = string.lower(tags['PatientName']) + + if string.find(patientName, 'david') ~= nil then + -- Only send patients whose name contains "David" + Delete(SendToModality(instanceId, 'sample')) + + else + -- Delete the patients that are not called "David" + Delete(instanceId) + end +end
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Samples/Lua/AutoroutingModification.lua Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,21 @@ +function OnStoredInstance(instanceId, tags, metadata) + -- Ignore the instances that result from a modification to avoid + -- infinite loops + if (metadata['ModifiedFrom'] == nil and + metadata['AnonymizedFrom'] == nil) then + + -- The tags to be replaced + local replace = {} + replace['StationName'] = 'My Medical Device' + replace['0031-1020'] = 'Some private tag' + + -- The tags to be removed + local remove = { 'MilitaryRank' } + + -- Modify the instance, send it, then delete the modified instance + Delete(SendToModality(ModifyInstance(instanceId, replace, remove, true), 'sample')) + + -- Delete the original instance + Delete(instanceId) + end +end
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Samples/Lua/CallDcm2Xml.lua Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,8 @@ +function OnStoredInstance(instanceId, tags, metadata) + -- Assume Latin1 encoding in dcm2xml + local args = {} + table.insert(args, '+Ca') + table.insert(args, 'latin-1') + + Delete(CallSystem(instanceId, 'dcm2xml', args)) +end
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Samples/Lua/CallWebService.js Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,80 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + **/ + + +/** + * This file is a simple echo Web service implemented using + * "node.js". Whenever it receives a POST HTTP query, it echoes its + * body both to stdout and to the client. Credentials are checked. + **/ + + +// Parameters of the ECHO server +var port = 8000; +var username = 'alice'; +var password = 'alicePassword'; + + +var http = require('http'); +var authorization = 'Basic ' + new Buffer(username + ':' + password).toString('base64') + +var server = http.createServer(function(req, response) { + // Check the credentials + if (req.headers.authorization != authorization) + { + console.log('Bad credentials, access not allowed'); + response.writeHead(401); + response.end(); + return; + } + + switch (req.method) + { + case 'POST': + { + var body = ''; + + req.on('data', function (data) { + response.write(data); + body += data; + }); + + req.on('end', function () { + console.log('Message received: ' + body); + response.end(); + }); + + break; + } + + default: + console.log('Method ' + req.method + ' is not supported by this ECHO Web service'); + response.writeHead(405, {'Allow': 'POST'}); + response.end(); + } +}); + +server.listen(port);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Samples/Lua/CallWebService.lua Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,24 @@ +-- This sample shows how to call a remote Web service whenever an +-- instance is received by Orthanc. For this sample to work, you have +-- to start the "CallWebService.js" script next to this file using +-- NodeJs. + +-- Download and install the JSON module for Lua by Jeffrey Friedl +-- http://regex.info/blog/lua/json +JSON = (loadstring(HttpGet('http://regex.info/code/JSON.lua'))) () + +SetHttpCredentials('alice', 'alicePassword') + +function OnStoredInstance(instanceId, tags, metadata) + -- Build the POST body + local info = {} + info['InstanceID'] = instanceId + info['PatientName'] = tags['PatientName'] + info['PatientID'] = tags['PatientID'] + + -- Send the POST request + local answer = HttpPost('http://localhost:8000/', JSON:encode(info)) + + -- The answer equals "ERROR" in case of an error + print('Web service called, answer received: ' .. answer) +end
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Samples/Lua/TransferSyntaxDisable.lua Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,29 @@ +function IsDeflatedTransferSyntaxAccepted(aet, ip) + return false +end + +function IsJpegTransferSyntaxAccepted(aet, ip) + return false +end + +function IsJpeg2000TransferSyntaxAccepted(aet, ip) + return false +end + +function IsJpegLosslessTransferSyntaxAccepted(aet, ip) + return false +end + +function IsJpipTransferSyntaxAccepted(aet, ip) + return false +end + +function IsMpeg2TransferSyntaxAccepted(aet, ip) + return false +end + +function IsRleTransferSyntaxAccepted(aet, ip) + return false +end + +print('All special transfer syntaxes are now disallowed')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Samples/Lua/TransferSyntaxEnable.lua Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,29 @@ +function IsDeflatedTransferSyntaxAccepted(aet, ip) + return true +end + +function IsJpegTransferSyntaxAccepted(aet, ip) + return true +end + +function IsJpeg2000TransferSyntaxAccepted(aet, ip) + return true +end + +function IsJpegLosslessTransferSyntaxAccepted(aet, ip) + return true +end + +function IsJpipTransferSyntaxAccepted(aet, ip) + return true +end + +function IsMpeg2TransferSyntaxAccepted(aet, ip) + return true +end + +function IsRleTransferSyntaxAccepted(aet, ip) + return true +end + +print('All special transfer syntaxes are now accepted')
--- a/Resources/Samples/OrthancClient/Basic/main.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/Samples/OrthancClient/Basic/main.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation @@ -54,6 +54,15 @@ OrthancClient::Series series(study.GetSeries(k)); std::cout << " Series: " << series.GetId() << std::endl; + if (series.Is3DImage()) + { + std::cout << " This is a 3D image whose voxel size is " + << series.GetVoxelSizeX() << " x " + << series.GetVoxelSizeY() << " x " + << series.GetVoxelSizeZ() << ", and slice thickness is " + << series.GetSliceThickness() << std::endl; + } + for (unsigned int l = 0; l < series.GetInstanceCount(); l++) { std::cout << " Instance: " << series.GetInstance(l).GetId() << std::endl;
--- a/Resources/Samples/OrthancClient/Vtk/main.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/Samples/OrthancClient/Vtk/main.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation
--- a/Resources/Samples/Python/AnonymizeAllPatients.py Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/Samples/Python/AnonymizeAllPatients.py Thu May 21 16:58:30 2015 +0200 @@ -2,8 +2,8 @@ # -*- coding: utf-8 -*- # Orthanc - A Lightweight, RESTful DICOM Store -# Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, -# Belgium +# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics +# Department, University Hospital 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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Samples/Python/ArchiveStudiesInTimeRange.py Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,78 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics +# Department, University Hospital 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/>. + + + +import os +import os.path +import sys +import RestToolbox + +def PrintHelp(): + print('Download ZIP archives for all the studies generated ' + 'during a given time range (according to the StudyDate tag)\n') + print('Usage: %s <URL> <StartDate> <EndDate> <TargetFolder>\n' % sys.argv[0]) + print('Example: %s http://localhost:8042/ 20150101 20151231 /tmp/\n' % sys.argv[0]) + exit(-1) + +def CheckIsDate(date): + if len(date) != 8 or not date.isdigit(): + print '"%s" is not a valid date!\n' % date + exit(-1) + + +if len(sys.argv) != 5: + PrintHelp() + +URL = sys.argv[1] +START = sys.argv[2] +END = sys.argv[3] +TARGET = sys.argv[4] + +CheckIsDate(START) +CheckIsDate(END) + +# Loop over the studies +for studyId in RestToolbox.DoGet('%s/studies' % URL): + # Retrieve the DICOM tags of the current study + study = RestToolbox.DoGet('%s/studies/%s' % (URL, studyId))['MainDicomTags'] + + # Retrieve the DICOM tags of the parent patient of this study + patient = RestToolbox.DoGet('%s/studies/%s/patient' % (URL, studyId))['MainDicomTags'] + + # Check that the StudyDate tag lies within the given range + studyDate = study['StudyDate'][:8] + if studyDate >= START and studyDate <= END: + # Create a filename + filename = '%s - %s %s - %s.zip' % (study['StudyDate'], + patient['PatientID'], + patient['PatientName'], + study['StudyDescription']) + + # Remove any non-ASCII character in the filename + filename = filename.encode('ascii', errors = 'replace') + + # Download the ZIP archive of the study + print('Downloading %s' % filename) + zipContent = RestToolbox.DoGet('%s/studies/%s/archive' % (URL, studyId)) + + # Write the ZIP archive at the proper location + with open(os.path.join(TARGET, filename), 'wb') as f: + f.write(zipContent)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Samples/Python/AutoClassify.py Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,124 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics +# Department, University Hospital 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/>. + + +import argparse +import time +import os +import os.path +import sys +import RestToolbox + +parser = argparse.ArgumentParser( + description = 'Automated classification of DICOM files from Orthanc.', + formatter_class = argparse.ArgumentDefaultsHelpFormatter) + +parser.add_argument('--host', default = 'localhost', + help = 'The host address that runs Orthanc') +parser.add_argument('--port', type = int, default = '8042', + help = 'The port number to which Orthanc is listening for the REST API') +parser.add_argument('--target', default = 'OrthancFiles', + help = 'The target directory where to store the DICOM files') +parser.add_argument('--all', action = 'store_true', + help = 'Replay the entire history on startup (disabled by default)') +parser.set_defaults(all = False) +parser.add_argument('--remove', action = 'store_true', + help = 'Remove DICOM files from Orthanc once classified (disabled by default)') +parser.set_defaults(remove = False) + + +def FixPath(p): + return p.encode('ascii', 'ignore').strip().decode() + +def GetTag(resource, tag): + if ('MainDicomTags' in resource and + tag in resource['MainDicomTags']): + return resource['MainDicomTags'][tag] + else: + return 'No' + tag + +def ClassifyInstance(instanceId): + # Extract the patient, study, series and instance information + instance = RestToolbox.DoGet('%s/instances/%s' % (URL, instanceId)) + series = RestToolbox.DoGet('%s/series/%s' % (URL, instance['ParentSeries'])) + study = RestToolbox.DoGet('%s/studies/%s' % (URL, series['ParentStudy'])) + patient = RestToolbox.DoGet('%s/patients/%s' % (URL, study['ParentPatient'])) + + # Construct a target path + a = '%s - %s' % (GetTag(patient, 'PatientID'), + GetTag(patient, 'PatientName')) + b = GetTag(study, 'StudyDescription') + c = '%s - %s' % (GetTag(series, 'Modality'), + GetTag(series, 'SeriesDescription')) + d = '%s.dcm' % GetTag(instance, 'SOPInstanceUID') + + p = os.path.join(args.target, FixPath(a), FixPath(b), FixPath(c)) + f = os.path.join(p, FixPath(d)) + + # Copy the DICOM file to the target path + print('Writing new DICOM file: %s' % f) + + try: + os.makedirs(p) + except: + # Already existing directory, ignore the error + pass + + dcm = RestToolbox.DoGet('%s/instances/%s/file' % (URL, instanceId)) + with open(f, 'wb') as g: + g.write(dcm) + + +# Parse the arguments +args = parser.parse_args() +URL = 'http://%s:%d' % (args.host, args.port) +print('Connecting to Orthanc on address: %s' % URL) + +# Compute the starting point for the changes loop +if args.all: + current = 0 +else: + current = RestToolbox.DoGet(URL + '/changes?last')['Last'] + +# Polling loop using the 'changes' API of Orthanc, waiting for the +# incoming of new DICOM files +while True: + r = RestToolbox.DoGet(URL + '/changes', { + 'since' : current, + 'limit' : 4 # Retrieve at most 4 changes at once + }) + + for change in r['Changes']: + # We are only interested interested in the arrival of new instances + if change['ChangeType'] == 'NewInstance': + try: + ClassifyInstance(change['ID']) + except: + print('Unable to write instance %s to the disk' % change['ID']) + + # If requested, remove the instance once it has been copied + if args.remove: + RestToolbox.DoDelete('%s/instances/%s' % (URL, change['ID'])) + + current = r['Last'] + + if r['Done']: + print('Everything has been processed: Waiting...') + time.sleep(1)
--- a/Resources/Samples/Python/ChangesLoop.py Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/Samples/Python/ChangesLoop.py Thu May 21 16:58:30 2015 +0200 @@ -1,8 +1,8 @@ #!/usr/bin/python # Orthanc - A Lightweight, RESTful DICOM Store -# Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, -# Belgium +# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics +# Department, University Hospital 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 @@ -54,7 +54,7 @@ # Remove the possible trailing characters due to DICOM padding patientName = patientName.strip() - print 'New instance received for patient "%s": "%s"' % (patientName, path) + print('New instance received for patient "%s": "%s"' % (patientName, path)) @@ -82,5 +82,5 @@ current = r['Last'] if r['Done']: - print "Everything has been processed: Waiting..." + print('Everything has been processed: Waiting...') time.sleep(1)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Samples/Python/ContinuousPatientAnonymization.py Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,99 @@ +#!/usr/bin/python + +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics +# Department, University Hospital 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/>. + + + +import time +import sys +import RestToolbox +import md5 + + +## +## Print help message +## + +if len(sys.argv) != 3: + print(""" +Sample script that anonymizes patients in real-time. A patient gets +anonymized as soon as she gets stable (i.e. when no DICOM instance has +been received for this patient for a sufficient amount of time - cf. +the configuration option "StableAge"). + +Usage: %s [hostname] [HTTP port] +For instance: %s localhost 8042 +""" % (sys.argv[0], sys.argv[0])) + exit(-1) + +URL = 'http://%s:%d' % (sys.argv[1], int(sys.argv[2])) + + + +## +## The following function is called whenever a patient gets stable +## + +COUNT = 1 + +def AnonymizePatient(path): + global URL + global COUNT + + patient = RestToolbox.DoGet(URL + path) + patientID = patient['MainDicomTags']['PatientID'] + + # Ignore anonymized patients + if not 'AnonymizedFrom' in patient: + print('Patient with ID "%s" is stabilized: anonymizing it...' % (patientID)) + + # The PatientID after anonymization is taken as the 8 first + # characters from the MD5 hash of the original PatientID + anonymizedID = md5.new(patientID).hexdigest()[:8] + anonymizedName = 'Anonymized patient %d' % COUNT + COUNT += 1 + + RestToolbox.DoPost(URL + path + '/anonymize', + { 'Replace' : { 'PatientID' : anonymizedID, + 'PatientName' : anonymizedName } }) + + # Delete the source patient after the anonymization + RestToolbox.DoDelete(URL + change['Path']) + + + +## +## Main loop that listens to the changes API. +## + +current = 0 +while True: + r = RestToolbox.DoGet(URL + '/changes', { + 'since' : current, + 'limit' : 4 # Retrieve at most 4 changes at once + }) + + for change in r['Changes']: + if change['ChangeType'] == 'StablePatient': + AnonymizePatient(change['Path']) + + current = r['Last'] + + if r['Done']: + print('Everything has been processed: Waiting...') + time.sleep(1)
--- a/Resources/Samples/Python/DownloadAnonymized.py Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/Samples/Python/DownloadAnonymized.py Thu May 21 16:58:30 2015 +0200 @@ -2,8 +2,8 @@ # -*- coding: utf-8 -*- # Orthanc - A Lightweight, RESTful DICOM Store -# Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, -# Belgium +# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics +# Department, University Hospital 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 @@ -42,7 +42,7 @@ if name.startswith('anonymized'): # Trigger the download - print 'Downloading %s' % name + print('Downloading %s' % name) zipContent = RestToolbox.DoGet('%s/patients/%s/archive' % (URL, patient)) f = open(os.path.join('/tmp', name + '.zip'), 'wb') f.write(zipContent)
--- a/Resources/Samples/Python/HighPerformanceAutoRouting.py Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/Samples/Python/HighPerformanceAutoRouting.py Thu May 21 16:58:30 2015 +0200 @@ -2,8 +2,8 @@ # -*- coding: utf-8 -*- # Orthanc - A Lightweight, RESTful DICOM Store -# Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, -# Belgium +# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics +# Department, University Hospital 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 @@ -107,7 +107,7 @@ break if len(instances) > 0: - print 'Sending a packet of %d instances' % len(instances) + print('Sending a packet of %d instances' % len(instances)) start = time.time() # Send all the instances with a single DICOM connexion @@ -118,11 +118,13 @@ RestToolbox.DoDelete('%s/instances/%s' % (URL, instance)) # Clear the log of the exported instances (to prevent the - # SQLite database from growing indefinitely) + # SQLite database from growing indefinitely). More simply, + # you could also set the "LogExportedResources" option to + # "false" in the configuration file since Orthanc 0.8.3. RestToolbox.DoDelete('%s/exports' % URL) end = time.time() - print 'The packet of %d instances has been sent in %d seconds' % (len(instances), end - start) + print('The packet of %d instances has been sent in %d seconds' % (len(instances), end - start)) # @@ -131,7 +133,7 @@ def PrintProgress(queue): while True: - print 'Current queue size: %d' % (queue.qsize()) + print('Current queue size: %d' % (queue.qsize())) time.sleep(1)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Samples/Python/Replicate.py Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,109 @@ +#!/usr/bin/python + +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics +# Department, University Hospital 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/>. + + +import base64 +import httplib2 +import json +import re +import sys + +URL_REGEX = re.compile('(http|https)://((.+?):(.+?)@|)(.*)') + + +if len(sys.argv) != 3: + print(""" +Script to copy the content of one Orthanc server to another Orthanc +server through their REST API. + +Usage: %s [SourceURI] [TargetURI] +For instance: %s http://orthanc:password@localhost:8042/ http://localhost:8043/ +""" % (sys.argv[0], sys.argv[0])) + exit(-1) + + + +def CreateHeaders(parsedUrl): + headers = { } + username = parsedUrl.group(3) + password = parsedUrl.group(4) + + if username != None and password != None: + # This is a custom reimplementation of the + # "Http.add_credentials()" method for Basic HTTP Access + # Authentication (for some weird reason, this method does not + # always work) + # http://en.wikipedia.org/wiki/Basic_access_authentication + headers['authorization'] = 'Basic ' + base64.b64encode(username + ':' + password) + + return headers + + +def GetBaseUrl(parsedUrl): + return '%s://%s' % (parsedUrl.group(1), parsedUrl.group(5)) + + +def DoGetString(url): + global URL_REGEX + parsedUrl = URL_REGEX.match(url) + headers = CreateHeaders(parsedUrl) + + h = httplib2.Http() + resp, content = h.request(GetBaseUrl(parsedUrl), 'GET', headers = headers) + + if resp.status == 200: + return content + else: + raise Exception('Unable to contact Orthanc at: ' + url) + + +def DoPostDicom(url, body): + global URL_REGEX + parsedUrl = URL_REGEX.match(url) + headers = CreateHeaders(parsedUrl) + headers['content-type'] = 'application/dicom' + + h = httplib2.Http() + resp, content = h.request(GetBaseUrl(parsedUrl), 'POST', + body = body, + headers = headers) + + if resp.status != 200: + raise Exception('Unable to contact Orthanc at: ' + url) + + +def _DecodeJson(s): + if (sys.version_info >= (3, 0)): + return json.loads(s.decode()) + else: + return json.loads(s) + + +def DoGetJson(url): + return _DecodeJson(DoGetString(url)) + + +SOURCE = sys.argv[1] +TARGET = sys.argv[2] + +for study in DoGetJson('%s/studies' % SOURCE): + print('Sending study %s...' % study) + for instance in DoGetJson('%s/studies/%s/instances' % (SOURCE, study)): + dicom = DoGetString('%s/instances/%s/file' % (SOURCE, instance['ID'])) + DoPostDicom('%s/instances' % TARGET, dicom)
--- a/Resources/Samples/Python/RestToolbox.py Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/Samples/Python/RestToolbox.py Thu May 21 16:58:30 2015 +0200 @@ -1,6 +1,6 @@ # Orthanc - A Lightweight, RESTful DICOM Store -# Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, -# Belgium +# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics +# Department, University Hospital 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 @@ -18,10 +18,27 @@ import httplib2 import json -from urllib import urlencode +import sys + +if (sys.version_info >= (3, 0)): + from urllib.parse import urlencode +else: + from urllib import urlencode + _credentials = None + +def _DecodeJson(s): + try: + if (sys.version_info >= (3, 0)): + return json.loads(s.decode()) + else: + return json.loads(s) + except: + return s + + def SetCredentials(username, password): global _credentials _credentials = (username, password) @@ -42,12 +59,9 @@ if not (resp.status in [ 200 ]): raise Exception(resp.status) elif not interpretAsJson: - return content + return content.decode() else: - try: - return json.loads(content) - except: - return content + return _DecodeJson(content) def _DoPutOrPost(uri, method, data, contentType): @@ -72,10 +86,7 @@ if not (resp.status in [ 200, 302 ]): raise Exception(resp.status) else: - try: - return json.loads(content) - except: - return content + return _DecodeJson(content) def DoDelete(uri): @@ -86,10 +97,7 @@ if not (resp.status in [ 200 ]): raise Exception(resp.status) else: - try: - return json.loads(content) - except: - return content + return _DecodeJson(content) def DoPut(uri, data = {}, contentType = ''):
--- a/Resources/Samples/Tools/CMakeLists.txt Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/Samples/Tools/CMakeLists.txt Thu May 21 16:58:30 2015 +0200 @@ -20,6 +20,9 @@ include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake) include(${ORTHANC_ROOT}/Resources/CMake/BoostConfiguration.cmake) include(${ORTHANC_ROOT}/Resources/CMake/ZlibConfiguration.cmake) +include(${ORTHANC_ROOT}/Resources/CMake/GoogleLogConfiguration.cmake) + +include_directories(${ORTHANC_ROOT}/OrthancCppClient/SharedLibrary/Laaw) add_library(CommonLibraries ${BOOST_SOURCES} @@ -27,8 +30,8 @@ ${ORTHANC_ROOT}/Core/OrthancException.cpp ${ORTHANC_ROOT}/Core/Toolbox.cpp ${ORTHANC_ROOT}/Core/Uuid.cpp - ${ORTHANC_ROOT}/Resources/md5/md5.c - ${ORTHANC_ROOT}/Resources/base64/base64.cpp + ${ORTHANC_ROOT}/Resources/ThirdParty/md5/md5.c + ${ORTHANC_ROOT}/Resources/ThirdParty/base64/base64.cpp ) add_executable(RecoverCompressedFile @@ -37,4 +40,4 @@ ${ORTHANC_ROOT}/Core/Compression/ZlibCompressor.cpp ) -target_link_libraries(RecoverCompressedFile CommonLibraries) +target_link_libraries(RecoverCompressedFile CommonLibraries GoogleLog)
--- a/Resources/Samples/Tools/RecoverCompressedFile.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/Samples/Tools/RecoverCompressedFile.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/Resources/Samples/WebApplications/DrawingDicomizer.js Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/Samples/WebApplications/DrawingDicomizer.js Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation
--- a/Resources/Samples/WebApplications/DrawingDicomizer/index.html Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/Samples/WebApplications/DrawingDicomizer/index.html Thu May 21 16:58:30 2015 +0200 @@ -15,7 +15,16 @@ <body> <canvas id="canvas" width="490" height="220"></canvas> <p> - Patient Name: <input type="text" id="patientName"></input> + Patient ID: <input type="text" id="patientID"></input> + </p> + <p> + Patient Name: <input type="text" id="patientName" value="HELLO^WORLD"></input> + </p> + <p> + Study Description: <input type="text" id="studyDescription" value="My Study"></input> + </p> + <p> + Series Description: <input type="text" id="seriesDescription" value="My Series"></input> </p> <p> <button id="submit">Submit</button>
--- a/Resources/Samples/WebApplications/DrawingDicomizer/orthanc.js Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/Samples/WebApplications/DrawingDicomizer/orthanc.js Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation @@ -24,23 +24,43 @@ * SOFTWARE. **/ +function guid4Block() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); +} + +function guid() { + return (guid4Block() + guid4Block() + '-' + guid4Block() + '-' + guid4Block() + '-' + + guid4Block() + '-' + guid4Block() + guid4Block() + guid4Block()); +} + $(document).ready(function() { + $('#patientID').val(guid()); + $('#submit').click(function(event) { var png = context.canvas.toDataURL(); $.ajax({ type: 'POST', url: '/orthanc/tools/create-dicom', + dataType: 'text', data: { + PatientID: $('#patientID').val(), PatientName: $('#patientName').val(), + StudyDescription: $('#studyDescription').val(), + SeriesDescription: $('#seriesDescription').val(), PixelData: png, - Modality: 'RX' + Modality: 'RX' + }, + success : function(msg) { + alert('Your drawing has been DICOM-ized!\n\n' + msg); + }, + error : function() { + alert('Error while DICOM-izing the drawing'); } - }) - .success(function( msg ) { - alert('Your drawing has been dicomized!\n\n' + msg); - }); + }); return false; });
--- a/Resources/Samples/WebApplications/NodeToolbox.js Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/Samples/WebApplications/NodeToolbox.js Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation @@ -35,15 +35,24 @@ opts.method = 'GET'; http.get(opts, function(response) { - response.setEncoding('utf-8'); - response.on('data', function(chunk) { - res.write(chunk); - }); - response.on('end', function() { + if (response.statusCode == 200) { + response.setEncoding('utf-8'); + response.on('data', function(chunk) { + res.write(chunk); + }); + response.on('end', function() { + res.end(); + }); + } else { + console.log('Got error on GET forwarding: ' + + response.statusCode + ' (' + path + ')'); + res.writeHead(response.statusCode); res.end(); - }); + } }).on('error', function(e) { - console.log('Got error on GET forwarding: ' + e.message + ' (' + path + ')'); + console.log('Unable to contact Orthanc: ' + e.message); + res.writeHead(503); // Service Unavailable + res.end(); }); } @@ -57,15 +66,24 @@ } var req = http.request(opts, function(response) { - response.setEncoding('utf-8'); - response.on('data', function(chunk) { - res.write(chunk); - }); - response.on('end', function() { + if (response.statusCode == 200) { + response.setEncoding('utf-8'); + response.on('data', function(chunk) { + res.write(chunk); + }); + response.on('end', function() { + res.end(); + }); + } else { + console.log('Got error on POST forwarding: ' + + response.statusCode + ' (' + path + ')'); + res.writeHead(response.statusCode); res.end(); - }); + } }).on('error', function(e) { - console.log('Got error on POST forwarding: ' + e.message + ' (' + path + ')'); + console.log('Unable to contact Orthanc: ' + e.message); + res.writeHead(503); // Service Unavailable + res.end(); }); req.write(body);
--- a/Resources/Toolbox.lua Wed Jun 25 15:34:40 2014 +0200 +++ b/Resources/Toolbox.lua Thu May 21 16:58:30 2015 +0200 @@ -6,7 +6,8 @@ --]] function PrintRecursive(s, l, i) -- recursive Print (structure, limit, indent) - l = (l) or 100; i = i or ""; -- default item limit, indent string + l = (l) or 100; -- default item limit + i = i or ""; -- indent string if (l<1) then print "ERROR: Item limit reached."; return l-1 end; local ts = type(s); if (ts ~= "table") then print (i,ts,s); return l-1 end @@ -18,4 +19,94 @@ return l end + + + +function _InitializeJob() + _job = {} +end + + +function _AccessJob() + return _job +end + + +function SendToModality(instanceId, modality) + if instanceId == nil then + error('Cannot send a nonexistent instance') + end + + table.insert(_job, { + Operation = 'store-scu', + Instance = instanceId, + Modality = modality + }) + return instanceId +end + + +function SendToPeer(instanceId, peer) + if instanceId == nil then + error('Cannot send a nonexistent instance') + end + + table.insert(_job, { + Operation = 'store-peer', + Instance = instanceId, + Peer = peer + }) + return instanceId +end + + +function Delete(instanceId) + if instanceId == nil then + error('Cannot delete a nonexistent instance') + end + + table.insert(_job, { + Operation = 'delete', + Instance = instanceId + }) + return nil -- Forbid chaining +end + + +function ModifyInstance(instanceId, replacements, removals, removePrivateTags) + if instanceId == nil then + error('Cannot modify a nonexistent instance') + end + + if instanceId == '' then + error('Cannot modify twice an instance'); + end + + table.insert(_job, { + Operation = 'modify', + Instance = instanceId, + Replace = replacements, + Remove = removals, + RemovePrivateTags = removePrivateTags + }) + return '' -- Chain with another operation +end + + +function CallSystem(instanceId, command, args) + if instanceId == nil then + error('Cannot modify a nonexistent instance') + end + + table.insert(_job, { + Operation = 'call-system', + Instance = instanceId, + Command = command, + Arguments = args + }) + + return instanceId +end + + print('Lua toolbox installed')
--- a/THANKS Wed Jun 25 15:34:40 2014 +0200 +++ b/THANKS Thu May 21 16:58:30 2015 +0200 @@ -8,8 +8,8 @@ complete and exempt or errors. -Code contributors ------------------ +Contributors +------------ * Cyril Paulus <cyril.paulus@student.ulg.ac.be>, for the build process and suggestions about the REST API. @@ -20,6 +20,19 @@ * 12maksqwe@gmail.com, for fixing issue #8. * Julien Nabet, for various suggestions to improve the source code. * Karsten Hilbert <Karsten.Hilbert@gmx.net>, for in-depth testing. +* Marek Swiecicki <mswiecicki@archimedic.pl>, for various suggestions + and sample DICOM files. +* Chris Hafey <chafey@gmail.com>, for suggesting many features and + improvements, for a Windows service+installer with .NET/NSIS. +* Manabu Tokunaga <manabu@lury.net>, for a Windows service with .NET. +* Vincent Kersten <vincent1234567@gmail.com>, for DICOMDIR in the GUI. +* Emsy Chan <emlscs@yahoo.com>, for various contributions + and sample DICOM files. +* Mikhail <mp39590@gmail.com>, for FreeBSD support. + + +Thanks also to all the contributors active in our Google Group: +https://groups.google.com/forum/#!forum/orthanc-users Debian/Ubuntu
--- a/UnitTestsSources/DicomMap.cpp Wed Jun 25 15:34:40 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,130 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - **/ - - -#include "PrecompiledHeadersUnitTests.h" -#include "gtest/gtest.h" - -#include "../Core/Uuid.h" -#include "../Core/OrthancException.h" -#include "../Core/DicomFormat/DicomMap.h" -#include "../Core/DicomFormat/DicomNullValue.h" - -#include <memory> - -using namespace Orthanc; - -TEST(DicomMap, MainTags) -{ - ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID)); - ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID, ResourceType_Patient)); - ASSERT_FALSE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID, ResourceType_Study)); - - ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_STUDY_INSTANCE_UID)); - ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_ACCESSION_NUMBER)); - ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_SERIES_INSTANCE_UID)); - ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_SOP_INSTANCE_UID)); - - std::set<DicomTag> s; - DicomMap::GetMainDicomTags(s); - ASSERT_TRUE(s.end() != s.find(DICOM_TAG_PATIENT_ID)); - ASSERT_TRUE(s.end() != s.find(DICOM_TAG_STUDY_INSTANCE_UID)); - ASSERT_TRUE(s.end() != s.find(DICOM_TAG_ACCESSION_NUMBER)); - ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SERIES_INSTANCE_UID)); - ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SOP_INSTANCE_UID)); - - DicomMap::GetMainDicomTags(s, ResourceType_Patient); - ASSERT_TRUE(s.end() != s.find(DICOM_TAG_PATIENT_ID)); - ASSERT_TRUE(s.end() == s.find(DICOM_TAG_STUDY_INSTANCE_UID)); - - DicomMap::GetMainDicomTags(s, ResourceType_Study); - ASSERT_TRUE(s.end() != s.find(DICOM_TAG_STUDY_INSTANCE_UID)); - ASSERT_TRUE(s.end() != s.find(DICOM_TAG_ACCESSION_NUMBER)); - ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID)); - - DicomMap::GetMainDicomTags(s, ResourceType_Series); - ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SERIES_INSTANCE_UID)); - ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID)); - - DicomMap::GetMainDicomTags(s, ResourceType_Instance); - ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SOP_INSTANCE_UID)); - ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID)); -} - - -TEST(DicomMap, Tags) -{ - DicomMap m; - ASSERT_FALSE(m.HasTag(DICOM_TAG_PATIENT_NAME)); - ASSERT_FALSE(m.HasTag(0x0010, 0x0010)); - m.SetValue(0x0010, 0x0010, "PatientName"); - ASSERT_TRUE(m.HasTag(DICOM_TAG_PATIENT_NAME)); - ASSERT_TRUE(m.HasTag(0x0010, 0x0010)); - - ASSERT_FALSE(m.HasTag(DICOM_TAG_PATIENT_ID)); - m.SetValue(DICOM_TAG_PATIENT_ID, "PatientID"); - ASSERT_TRUE(m.HasTag(0x0010, 0x0020)); - m.SetValue(DICOM_TAG_PATIENT_ID, "PatientID2"); - ASSERT_EQ("PatientID2", m.GetValue(0x0010, 0x0020).AsString()); - - m.Remove(DICOM_TAG_PATIENT_ID); - ASSERT_THROW(m.GetValue(0x0010, 0x0020), OrthancException); - - std::auto_ptr<DicomMap> mm(m.Clone()); - ASSERT_EQ("PatientName", mm->GetValue(DICOM_TAG_PATIENT_NAME).AsString()); - - m.SetValue(DICOM_TAG_PATIENT_ID, "Hello"); - ASSERT_THROW(mm->GetValue(DICOM_TAG_PATIENT_ID), OrthancException); - mm->CopyTagIfExists(m, DICOM_TAG_PATIENT_ID); - ASSERT_EQ("Hello", mm->GetValue(DICOM_TAG_PATIENT_ID).AsString()); - - DicomNullValue v; - ASSERT_TRUE(v.IsNull()); -} - - -TEST(DicomMap, FindTemplates) -{ - DicomMap m; - - DicomMap::SetupFindPatientTemplate(m); - ASSERT_TRUE(m.HasTag(DICOM_TAG_PATIENT_ID)); - - DicomMap::SetupFindStudyTemplate(m); - ASSERT_TRUE(m.HasTag(DICOM_TAG_STUDY_INSTANCE_UID)); - ASSERT_TRUE(m.HasTag(DICOM_TAG_ACCESSION_NUMBER)); - - DicomMap::SetupFindSeriesTemplate(m); - ASSERT_TRUE(m.HasTag(DICOM_TAG_SERIES_INSTANCE_UID)); - - DicomMap::SetupFindInstanceTemplate(m); - ASSERT_TRUE(m.HasTag(DICOM_TAG_SOP_INSTANCE_UID)); -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/UnitTestsSources/DicomMapTests.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,206 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "PrecompiledHeadersUnitTests.h" +#include "gtest/gtest.h" + +#include "../Core/Uuid.h" +#include "../Core/OrthancException.h" +#include "../Core/DicomFormat/DicomMap.h" +#include "../Core/DicomFormat/DicomNullValue.h" +#include "../OrthancServer/FromDcmtkBridge.h" + +#include <memory> + +using namespace Orthanc; + +TEST(DicomMap, MainTags) +{ + ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID)); + ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID, ResourceType_Patient)); + ASSERT_FALSE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID, ResourceType_Study)); + + ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_STUDY_INSTANCE_UID)); + ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_ACCESSION_NUMBER)); + ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_SERIES_INSTANCE_UID)); + ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_SOP_INSTANCE_UID)); + + std::set<DicomTag> s; + DicomMap::GetMainDicomTags(s); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_PATIENT_ID)); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_STUDY_INSTANCE_UID)); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_ACCESSION_NUMBER)); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SERIES_INSTANCE_UID)); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SOP_INSTANCE_UID)); + + DicomMap::GetMainDicomTags(s, ResourceType_Patient); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_PATIENT_ID)); + ASSERT_TRUE(s.end() == s.find(DICOM_TAG_STUDY_INSTANCE_UID)); + + DicomMap::GetMainDicomTags(s, ResourceType_Study); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_STUDY_INSTANCE_UID)); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_ACCESSION_NUMBER)); + ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID)); + + DicomMap::GetMainDicomTags(s, ResourceType_Series); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SERIES_INSTANCE_UID)); + ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID)); + + DicomMap::GetMainDicomTags(s, ResourceType_Instance); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SOP_INSTANCE_UID)); + ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID)); +} + + +TEST(DicomMap, Tags) +{ + std::set<DicomTag> s; + + DicomMap m; + m.GetTags(s); + ASSERT_EQ(0, s.size()); + + ASSERT_FALSE(m.HasTag(DICOM_TAG_PATIENT_NAME)); + ASSERT_FALSE(m.HasTag(0x0010, 0x0010)); + m.SetValue(0x0010, 0x0010, "PatientName"); + ASSERT_TRUE(m.HasTag(DICOM_TAG_PATIENT_NAME)); + ASSERT_TRUE(m.HasTag(0x0010, 0x0010)); + + m.GetTags(s); + ASSERT_EQ(1, s.size()); + ASSERT_EQ(DICOM_TAG_PATIENT_NAME, *s.begin()); + + ASSERT_FALSE(m.HasTag(DICOM_TAG_PATIENT_ID)); + m.SetValue(DICOM_TAG_PATIENT_ID, "PatientID"); + ASSERT_TRUE(m.HasTag(0x0010, 0x0020)); + m.SetValue(DICOM_TAG_PATIENT_ID, "PatientID2"); + ASSERT_EQ("PatientID2", m.GetValue(0x0010, 0x0020).AsString()); + + m.GetTags(s); + ASSERT_EQ(2, s.size()); + + m.Remove(DICOM_TAG_PATIENT_ID); + ASSERT_THROW(m.GetValue(0x0010, 0x0020), OrthancException); + + m.GetTags(s); + ASSERT_EQ(1, s.size()); + ASSERT_EQ(DICOM_TAG_PATIENT_NAME, *s.begin()); + + std::auto_ptr<DicomMap> mm(m.Clone()); + ASSERT_EQ("PatientName", mm->GetValue(DICOM_TAG_PATIENT_NAME).AsString()); + + m.SetValue(DICOM_TAG_PATIENT_ID, "Hello"); + ASSERT_THROW(mm->GetValue(DICOM_TAG_PATIENT_ID), OrthancException); + mm->CopyTagIfExists(m, DICOM_TAG_PATIENT_ID); + ASSERT_EQ("Hello", mm->GetValue(DICOM_TAG_PATIENT_ID).AsString()); + + DicomNullValue v; + ASSERT_TRUE(v.IsNull()); +} + + +TEST(DicomMap, FindTemplates) +{ + DicomMap m; + + DicomMap::SetupFindPatientTemplate(m); + ASSERT_TRUE(m.HasTag(DICOM_TAG_PATIENT_ID)); + + DicomMap::SetupFindStudyTemplate(m); + ASSERT_TRUE(m.HasTag(DICOM_TAG_STUDY_INSTANCE_UID)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_ACCESSION_NUMBER)); + + DicomMap::SetupFindSeriesTemplate(m); + ASSERT_TRUE(m.HasTag(DICOM_TAG_SERIES_INSTANCE_UID)); + + DicomMap::SetupFindInstanceTemplate(m); + ASSERT_TRUE(m.HasTag(DICOM_TAG_SOP_INSTANCE_UID)); +} + + + + +static void TestModule(ResourceType level, + DicomModule module) +{ + std::set<DicomTag> moduleTags, main; + DicomTag::GetTagsForModule(moduleTags, module); + DicomMap::GetMainDicomTags(main, level); + + // The main dicom tags are a subset of the module + for (std::set<DicomTag>::const_iterator it = main.begin(); it != main.end(); ++it) + { + bool ok = moduleTags.find(*it) != moduleTags.end(); + + // Exceptions for the Series level + /*if ((// + *it == DicomTag(0x, 0x) && + level == ResourceType_Series)) + { + ok = true; + }*/ + + // Exceptions for the Instance level + if ((/* Accession number, from Image module */ + *it == DicomTag(0x0020, 0x0012) && + level == ResourceType_Instance) || + (/* Image Index, from PET Image module */ + *it == DicomTag(0x0054, 0x1330) && + level == ResourceType_Instance) || + (/* Temporal Position Identifier, from MR Image module */ + *it == DicomTag(0x0020, 0x0100) && + level == ResourceType_Instance) || + (/* Number of Frames, from Multi-frame module attributes, related to Image IOD */ + *it == DicomTag(0x0028, 0x0008) && + level == ResourceType_Instance )) + { + ok = true; + } + + if (!ok) + { + std::cout << it->Format() << ": " << FromDcmtkBridge::GetName(*it) + << " not expected at level " << EnumerationToString(level) << std::endl; + } + + EXPECT_TRUE(ok); + } +} + + +TEST(DicomMap, Modules) +{ + TestModule(ResourceType_Patient, DicomModule_Patient); + TestModule(ResourceType_Study, DicomModule_Study); + //TestModule(ResourceType_Series, DicomModule_Series); // TODO + TestModule(ResourceType_Instance, DicomModule_Instance); +}
--- a/UnitTestsSources/FileStorage.cpp Wed Jun 25 15:34:40 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,228 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - **/ - - -#include "PrecompiledHeadersUnitTests.h" -#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; - - -static void StringToVector(std::vector<uint8_t>& v, - const std::string& s) -{ - v.resize(s.size()); - for (size_t i = 0; i < s.size(); i++) - v[i] = s[i]; -} - - -TEST(FileStorage, Basic) -{ - FileStorage s("UnitTestsStorage"); - - 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())); - ASSERT_EQ(s.GetCompressedSize(uid), data.size()); -} - -TEST(FileStorage, Basic2) -{ - FileStorage s("UnitTestsStorage"); - - std::vector<uint8_t> data; - StringToVector(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())); - ASSERT_EQ(s.GetCompressedSize(uid), data.size()); -} - -TEST(FileStorage, EndToEnd) -{ - FileStorage s("UnitTestsStorage"); - 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("UnitTestsStorage"); - 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("UnitTestsStorage"); - 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, NoCompression2) -{ - FileStorage s("UnitTestsStorage"); - CompressedFileStorageAccessor accessor(s); - - accessor.SetCompressionForNextOperations(CompressionType_None); - std::vector<uint8_t> data; - StringToVector(data, "Hello world"); - FileInfo info = accessor.Write(data, FileContentType_Dicom); - - std::string r; - accessor.Read(r, info.GetUuid()); - - ASSERT_EQ(0, memcmp(&r[0], &data[0], data.size())); - 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("UnitTestsStorage"); - 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("UnitTestsStorage"); - 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/UnitTestsSources/FileStorageTests.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,233 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "PrecompiledHeadersUnitTests.h" +#include "gtest/gtest.h" + +#include <ctype.h> +#include <glog/logging.h> + +#include "../Core/FileStorage/FilesystemStorage.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; + + +static void StringToVector(std::vector<uint8_t>& v, + const std::string& s) +{ + v.resize(s.size()); + for (size_t i = 0; i < s.size(); i++) + v[i] = s[i]; +} + + +TEST(FilesystemStorage, Basic) +{ + FilesystemStorage s("UnitTestsStorage"); + + std::string data = Toolbox::GenerateUuid(); + std::string uid = Toolbox::GenerateUuid(); + s.Create(uid.c_str(), &data[0], data.size(), FileContentType_Unknown); + std::string d; + s.Read(d, uid, FileContentType_Unknown); + ASSERT_EQ(d.size(), data.size()); + ASSERT_FALSE(memcmp(&d[0], &data[0], data.size())); + ASSERT_EQ(s.GetSize(uid), data.size()); +} + +TEST(FilesystemStorage, Basic2) +{ + FilesystemStorage s("UnitTestsStorage"); + + std::vector<uint8_t> data; + StringToVector(data, Toolbox::GenerateUuid()); + std::string uid = Toolbox::GenerateUuid(); + s.Create(uid.c_str(), &data[0], data.size(), FileContentType_Unknown); + std::string d; + s.Read(d, uid, FileContentType_Unknown); + ASSERT_EQ(d.size(), data.size()); + ASSERT_FALSE(memcmp(&d[0], &data[0], data.size())); + ASSERT_EQ(s.GetSize(uid), data.size()); +} + +TEST(FilesystemStorage, EndToEnd) +{ + FilesystemStorage s("UnitTestsStorage"); + s.Clear(); + + std::list<std::string> u; + for (unsigned int i = 0; i < 10; i++) + { + std::string t = Toolbox::GenerateUuid(); + std::string uid = Toolbox::GenerateUuid(); + s.Create(uid.c_str(), &t[0], t.size(), FileContentType_Unknown); + u.push_back(uid); + } + + 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, FileContentType_Unknown); + } + + s.ListAllFiles(ss); + ASSERT_EQ(5u, ss.size()); + + s.Clear(); + s.ListAllFiles(ss); + ASSERT_EQ(0u, ss.size()); +} + + +TEST(FileStorageAccessor, Simple) +{ + FilesystemStorage s("UnitTestsStorage"); + FileStorageAccessor accessor(s); + + std::string data = "Hello world"; + FileInfo info = accessor.Write(data, FileContentType_Dicom); + + std::string r; + accessor.Read(r, info.GetUuid(), FileContentType_Unknown); + + 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) +{ + FilesystemStorage s("UnitTestsStorage"); + 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(), FileContentType_Unknown); + + 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, NoCompression2) +{ + FilesystemStorage s("UnitTestsStorage"); + CompressedFileStorageAccessor accessor(s); + + accessor.SetCompressionForNextOperations(CompressionType_None); + std::vector<uint8_t> data; + StringToVector(data, "Hello world"); + FileInfo info = accessor.Write(data, FileContentType_Dicom); + + std::string r; + accessor.Read(r, info.GetUuid(), FileContentType_Unknown); + + ASSERT_EQ(0, memcmp(&r[0], &data[0], data.size())); + 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) +{ + FilesystemStorage s("UnitTestsStorage"); + 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(), FileContentType_Unknown); + + ASSERT_EQ(data, r); + ASSERT_EQ(CompressionType_Zlib, info.GetCompressionType()); + ASSERT_EQ(11u, info.GetUncompressedSize()); + ASSERT_EQ(FileContentType_Dicom, info.GetContentType()); +} + + +TEST(FileStorageAccessor, Mix) +{ + FilesystemStorage s("UnitTestsStorage"); + 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(), FileContentType_Unknown); + ASSERT_EQ(compressedData, r); + + accessor.SetCompressionForNextOperations(CompressionType_None); + accessor.Read(r, compressedInfo.GetUuid(), FileContentType_Unknown); + ASSERT_NE(compressedData, r); + + /* + // This test is too slow on Windows + accessor.SetCompressionForNextOperations(CompressionType_Zlib); + ASSERT_THROW(accessor.Read(r, uncompressedInfo.GetUuid(), FileContentType_Unknown), OrthancException); + */ +}
--- a/UnitTestsSources/FromDcmtk.cpp Wed Jun 25 15:34:40 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,147 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - **/ - - -#include "PrecompiledHeadersUnitTests.h" -#include "gtest/gtest.h" - -#include "../OrthancServer/FromDcmtkBridge.h" -#include "../OrthancServer/OrthancInitialization.h" -#include "../OrthancServer/DicomModification.h" -#include "../Core/OrthancException.h" -#include "../Core/ImageFormats/ImageBuffer.h" -#include "../Core/ImageFormats/PngReader.h" -#include "../Core/ImageFormats/PngWriter.h" - -using namespace Orthanc; - -TEST(DicomFormat, Tag) -{ - ASSERT_EQ("PatientName", FromDcmtkBridge::GetName(DicomTag(0x0010, 0x0010))); - - DicomTag t = FromDcmtkBridge::ParseTag("SeriesDescription"); - ASSERT_EQ(0x0008, t.GetGroup()); - ASSERT_EQ(0x103E, t.GetElement()); - - t = FromDcmtkBridge::ParseTag("0020-e040"); - ASSERT_EQ(0x0020, t.GetGroup()); - ASSERT_EQ(0xe040, t.GetElement()); - - // Test ==() and !=() operators - ASSERT_TRUE(DICOM_TAG_PATIENT_ID == DicomTag(0x0010, 0x0020)); - ASSERT_FALSE(DICOM_TAG_PATIENT_ID != DicomTag(0x0010, 0x0020)); -} - - -TEST(DicomModification, Basic) -{ - DicomModification m; - m.SetupAnonymization(); - //m.SetLevel(DicomRootLevel_Study); - //m.Replace(DICOM_TAG_PATIENT_ID, "coucou"); - //m.Replace(DICOM_TAG_PATIENT_NAME, "coucou"); - - ParsedDicomFile o; - o.SaveToFile("UnitTestsResults/anon.dcm"); - - for (int i = 0; i < 10; i++) - { - char b[1024]; - sprintf(b, "UnitTestsResults/anon%06d.dcm", i); - std::auto_ptr<ParsedDicomFile> f(o.Clone()); - if (i > 4) - o.Replace(DICOM_TAG_SERIES_INSTANCE_UID, "coucou"); - m.Apply(*f); - f->SaveToFile(b); - } -} - - -#include <dcmtk/dcmdata/dcuid.h> - -TEST(DicomModification, Png) -{ - // Red dot in http://en.wikipedia.org/wiki/Data_URI_scheme (RGBA image) - std::string s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; - - std::string m, c; - Toolbox::DecodeDataUriScheme(m, c, s); - - ASSERT_EQ("image/png", m); - ASSERT_EQ(116, c.size()); - - std::string cc; - Toolbox::DecodeBase64(cc, c); - PngReader reader; - reader.ReadFromMemory(cc); - - ASSERT_EQ(5, reader.GetHeight()); - ASSERT_EQ(5, reader.GetWidth()); - ASSERT_EQ(PixelFormat_RGBA32, reader.GetFormat()); - - ParsedDicomFile o; - o.EmbedImage(s); - o.SaveToFile("UnitTestsResults/png1.dcm"); - - // Red dot, without alpha channel - s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDTcIn2+8BgAAACJJREFUCNdj/P//PwMjIwME/P/P+J8BBTAxEOL/R9Lx/z8AynoKAXOeiV8AAAAASUVORK5CYII="; - o.EmbedImage(s); - o.SaveToFile("UnitTestsResults/png2.dcm"); - - // Check box in Graylevel8 - s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDDcB53FulQAAAElJREFUGNNtj0sSAEEEQ1+U+185s1CtmRkblQ9CZldsKHJDk6DLGLJa6chjh0ooQmpjXMM86zPwydGEj6Ed/UGykkEM8X+p3u8/8LcOJIWLGeMAAAAASUVORK5CYII="; - o.EmbedImage(s); - //o.Replace(DICOM_TAG_SOP_CLASS_UID, UID_DigitalXRayImageStorageForProcessing); - o.SaveToFile("UnitTestsResults/png3.dcm"); - - - { - // Gradient in Graylevel16 - - ImageBuffer img; - img.SetWidth(256); - img.SetHeight(256); - img.SetFormat(PixelFormat_Grayscale16); - - int v = 0; - for (unsigned int y = 0; y < img.GetHeight(); y++) - { - uint16_t *p = reinterpret_cast<uint16_t*>(img.GetAccessor().GetRow(y)); - for (unsigned int x = 0; x < img.GetWidth(); x++, p++, v++) - { - *p = v; - } - } - - o.EmbedImage(img.GetAccessor()); - o.SaveToFile("UnitTestsResults/png4.dcm"); - } -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/UnitTestsSources/FromDcmtkTests.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,296 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "PrecompiledHeadersUnitTests.h" +#include "gtest/gtest.h" + +#include "../OrthancServer/FromDcmtkBridge.h" +#include "../OrthancServer/OrthancInitialization.h" +#include "../OrthancServer/DicomModification.h" +#include "../Core/OrthancException.h" +#include "../Core/ImageFormats/ImageBuffer.h" +#include "../Core/ImageFormats/PngReader.h" +#include "../Core/ImageFormats/PngWriter.h" +#include "../Core/Uuid.h" +#include "../Resources/EncodingTests.h" + +using namespace Orthanc; + +TEST(DicomFormat, Tag) +{ + ASSERT_EQ("PatientName", FromDcmtkBridge::GetName(DicomTag(0x0010, 0x0010))); + + DicomTag t = FromDcmtkBridge::ParseTag("SeriesDescription"); + ASSERT_EQ(0x0008, t.GetGroup()); + ASSERT_EQ(0x103E, t.GetElement()); + + t = FromDcmtkBridge::ParseTag("0020-e040"); + ASSERT_EQ(0x0020, t.GetGroup()); + ASSERT_EQ(0xe040, t.GetElement()); + + // Test ==() and !=() operators + ASSERT_TRUE(DICOM_TAG_PATIENT_ID == DicomTag(0x0010, 0x0020)); + ASSERT_FALSE(DICOM_TAG_PATIENT_ID != DicomTag(0x0010, 0x0020)); +} + + +TEST(DicomModification, Basic) +{ + DicomModification m; + m.SetupAnonymization(); + //m.SetLevel(DicomRootLevel_Study); + //m.Replace(DICOM_TAG_PATIENT_ID, "coucou"); + //m.Replace(DICOM_TAG_PATIENT_NAME, "coucou"); + + ParsedDicomFile o; + o.SaveToFile("UnitTestsResults/anon.dcm"); + + for (int i = 0; i < 10; i++) + { + char b[1024]; + sprintf(b, "UnitTestsResults/anon%06d.dcm", i); + std::auto_ptr<ParsedDicomFile> f(o.Clone()); + if (i > 4) + o.Replace(DICOM_TAG_SERIES_INSTANCE_UID, "coucou"); + m.Apply(*f); + f->SaveToFile(b); + } +} + + +TEST(DicomModification, Anonymization) +{ + ASSERT_EQ(DICOM_TAG_PATIENT_NAME, FromDcmtkBridge::ParseTag("PatientName")); + + const DicomTag privateTag(0x0045, 0x0010); + const DicomTag privateTag2(FromDcmtkBridge::ParseTag("0031-1020")); + ASSERT_TRUE(FromDcmtkBridge::IsPrivateTag(privateTag)); + ASSERT_TRUE(FromDcmtkBridge::IsPrivateTag(privateTag2)); + ASSERT_EQ(0x0031, privateTag2.GetGroup()); + ASSERT_EQ(0x1020, privateTag2.GetElement()); + + std::string s; + ParsedDicomFile o; + o.Replace(DICOM_TAG_PATIENT_NAME, "coucou"); + ASSERT_FALSE(o.GetTagValue(s, privateTag)); + o.Insert(privateTag, "private tag"); + ASSERT_TRUE(o.GetTagValue(s, privateTag)); + ASSERT_STREQ("private tag", s.c_str()); + + ASSERT_FALSE(o.GetTagValue(s, privateTag2)); + ASSERT_THROW(o.Replace(privateTag2, "hello", DicomReplaceMode_ThrowIfAbsent), OrthancException); + ASSERT_FALSE(o.GetTagValue(s, privateTag2)); + o.Replace(privateTag2, "hello", DicomReplaceMode_IgnoreIfAbsent); + ASSERT_FALSE(o.GetTagValue(s, privateTag2)); + o.Replace(privateTag2, "hello", DicomReplaceMode_InsertIfAbsent); + ASSERT_TRUE(o.GetTagValue(s, privateTag2)); + ASSERT_STREQ("hello", s.c_str()); + o.Replace(privateTag2, "hello world"); + ASSERT_TRUE(o.GetTagValue(s, privateTag2)); + ASSERT_STREQ("hello world", s.c_str()); + + ASSERT_TRUE(o.GetTagValue(s, DICOM_TAG_PATIENT_NAME)); + ASSERT_FALSE(Toolbox::IsUuid(s)); + + DicomModification m; + m.SetupAnonymization(); + m.Keep(privateTag); + + m.Apply(o); + + ASSERT_TRUE(o.GetTagValue(s, DICOM_TAG_PATIENT_NAME)); + ASSERT_TRUE(Toolbox::IsUuid(s)); + ASSERT_TRUE(o.GetTagValue(s, privateTag)); + ASSERT_STREQ("private tag", s.c_str()); + + m.SetupAnonymization(); + m.Apply(o); + ASSERT_FALSE(o.GetTagValue(s, privateTag)); +} + + +#include <dcmtk/dcmdata/dcuid.h> + +TEST(DicomModification, Png) +{ + // Red dot in http://en.wikipedia.org/wiki/Data_URI_scheme (RGBA image) + std::string s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; + + std::string m, c; + Toolbox::DecodeDataUriScheme(m, c, s); + + ASSERT_EQ("image/png", m); + ASSERT_EQ(116, c.size()); + + std::string cc; + Toolbox::DecodeBase64(cc, c); + PngReader reader; + reader.ReadFromMemory(cc); + + ASSERT_EQ(5, reader.GetHeight()); + ASSERT_EQ(5, reader.GetWidth()); + ASSERT_EQ(PixelFormat_RGBA32, reader.GetFormat()); + + ParsedDicomFile o; + o.EmbedImage(s); + o.SaveToFile("UnitTestsResults/png1.dcm"); + + // Red dot, without alpha channel + s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDTcIn2+8BgAAACJJREFUCNdj/P//PwMjIwME/P/P+J8BBTAxEOL/R9Lx/z8AynoKAXOeiV8AAAAASUVORK5CYII="; + o.EmbedImage(s); + o.SaveToFile("UnitTestsResults/png2.dcm"); + + // Check box in Graylevel8 + s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDDcB53FulQAAAElJREFUGNNtj0sSAEEEQ1+U+185s1CtmRkblQ9CZldsKHJDk6DLGLJa6chjh0ooQmpjXMM86zPwydGEj6Ed/UGykkEM8X+p3u8/8LcOJIWLGeMAAAAASUVORK5CYII="; + o.EmbedImage(s); + //o.Replace(DICOM_TAG_SOP_CLASS_UID, UID_DigitalXRayImageStorageForProcessing); + o.SaveToFile("UnitTestsResults/png3.dcm"); + + + { + // Gradient in Graylevel16 + + ImageBuffer img; + img.SetWidth(256); + img.SetHeight(256); + img.SetFormat(PixelFormat_Grayscale16); + + int v = 0; + for (unsigned int y = 0; y < img.GetHeight(); y++) + { + uint16_t *p = reinterpret_cast<uint16_t*>(img.GetAccessor().GetRow(y)); + for (unsigned int x = 0; x < img.GetWidth(); x++, p++, v++) + { + *p = v; + } + } + + o.EmbedImage(img.GetAccessor()); + o.SaveToFile("UnitTestsResults/png4.dcm"); + } +} + + +TEST(FromDcmtkBridge, Encodings1) +{ + for (unsigned int i = 0; i < testEncodingsCount; i++) + { + std::string source(testEncodingsEncoded[i]); + std::string expected(testEncodingsExpected[i]); + std::string s = Toolbox::ConvertToUtf8(source, testEncodings[i]); + std::cout << EnumerationToString(testEncodings[i]) << std::endl; + EXPECT_EQ(expected, s); + } +} + + +TEST(FromDcmtkBridge, Enumerations) +{ + Encoding e; + + ASSERT_FALSE(GetDicomEncoding(e, "")); + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 6")); ASSERT_EQ(Encoding_Utf8, e); + + // http://www.dabsoft.ch/dicom/3/C.12.1.1.2/ - Table C.12-2 + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 100")); ASSERT_EQ(Encoding_Latin1, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 101")); ASSERT_EQ(Encoding_Latin2, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 109")); ASSERT_EQ(Encoding_Latin3, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 110")); ASSERT_EQ(Encoding_Latin4, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 144")); ASSERT_EQ(Encoding_Cyrillic, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 127")); ASSERT_EQ(Encoding_Arabic, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 126")); ASSERT_EQ(Encoding_Greek, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 138")); ASSERT_EQ(Encoding_Hebrew, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 148")); ASSERT_EQ(Encoding_Latin5, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 13")); ASSERT_EQ(Encoding_Japanese, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 166")); ASSERT_EQ(Encoding_Thai, e); + + // http://www.dabsoft.ch/dicom/3/C.12.1.1.2/ - Table C.12-3 + ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 6")); ASSERT_EQ(Encoding_Utf8, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 100")); ASSERT_EQ(Encoding_Latin1, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 101")); ASSERT_EQ(Encoding_Latin2, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 109")); ASSERT_EQ(Encoding_Latin3, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 110")); ASSERT_EQ(Encoding_Latin4, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 144")); ASSERT_EQ(Encoding_Cyrillic, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 127")); ASSERT_EQ(Encoding_Arabic, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 126")); ASSERT_EQ(Encoding_Greek, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 138")); ASSERT_EQ(Encoding_Hebrew, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 148")); ASSERT_EQ(Encoding_Latin5, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 13")); ASSERT_EQ(Encoding_Japanese, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 166")); ASSERT_EQ(Encoding_Thai, e); + + // http://www.dabsoft.ch/dicom/3/C.12.1.1.2/ - Table C.12-4 + ASSERT_FALSE(GetDicomEncoding(e, "ISO 2022 IR 87")); //ASSERT_EQ(Encoding_JapaneseKanji, e); + ASSERT_FALSE(GetDicomEncoding(e, "ISO 2022 IR 159")); //ASSERT_EQ(Encoding_JapaneseKanjiSupplementary, e); + ASSERT_FALSE(GetDicomEncoding(e, "ISO 2022 IR 149")); //ASSERT_EQ(Encoding_Korean, e); + + // http://www.dabsoft.ch/dicom/3/C.12.1.1.2/ - Table C.12-5 + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 192")); ASSERT_EQ(Encoding_Utf8, e); + ASSERT_TRUE(GetDicomEncoding(e, "GB18030")); ASSERT_EQ(Encoding_Chinese, e); +} + + +TEST(FromDcmtkBridge, Encodings3) +{ + for (unsigned int i = 0; i < testEncodingsCount; i++) + { + std::cout << EnumerationToString(testEncodings[i]) << std::endl; + std::string dicom; + + { + ParsedDicomFile f; + f.SetEncoding(testEncodings[i]); + f.Insert(DICOM_TAG_PATIENT_NAME, testEncodingsEncoded[i]); + f.SaveToMemoryBuffer(dicom); + } + + if (testEncodings[i] != Encoding_Windows1251) + { + ParsedDicomFile g(dicom); + + if (testEncodings[i] != Encoding_Ascii) + { + ASSERT_EQ(testEncodings[i], g.GetEncoding()); + } + + std::string tag; + ASSERT_TRUE(g.GetTagValue(tag, DICOM_TAG_PATIENT_NAME)); + ASSERT_EQ(std::string(testEncodingsExpected[i]), tag); + } + } +} + + +TEST(FromDcmtkBridge, VR) +{ + ASSERT_TRUE(FromDcmtkBridge::IsPNValueRepresentation(DICOM_TAG_PATIENT_NAME)); + ASSERT_FALSE(FromDcmtkBridge::IsPNValueRepresentation(DICOM_TAG_PATIENT_ID)); +}
--- a/UnitTestsSources/ImageProcessingTests.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/UnitTestsSources/ImageProcessingTests.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -51,6 +51,7 @@ m.SetValue(DICOM_TAG_BITS_STORED, "12"); m.SetValue(DICOM_TAG_HIGH_BIT, "11"); m.SetValue(DICOM_TAG_PIXEL_REPRESENTATION, "0"); + m.SetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2"); DicomImageInformation info(m); PixelFormat format; @@ -70,6 +71,7 @@ m.SetValue(DICOM_TAG_BITS_STORED, "16"); m.SetValue(DICOM_TAG_HIGH_BIT, "15"); m.SetValue(DICOM_TAG_PIXEL_REPRESENTATION, "1"); + m.SetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2"); DicomImageInformation info(m); PixelFormat format;
--- a/UnitTestsSources/JpegLossless.cpp Wed Jun 25 15:34:40 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,54 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - **/ - - -#include "PrecompiledHeadersUnitTests.h" -#include "gtest/gtest.h" - -#include "../OrthancServer/Internals/DicomImageDecoder.h" - -#if ORTHANC_JPEG_LOSSLESS_ENABLED == 1 - -#include <dcmtk/dcmdata/dcfilefo.h> - -#include "../OrthancServer/ParsedDicomFile.h" -#include "../Core/OrthancException.h" -#include "../Core/ImageFormats/ImageBuffer.h" -#include "../Core/ImageFormats/PngWriter.h" - -using namespace Orthanc; - - - -// TODO Write a test - - -#endif
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/UnitTestsSources/JpegLosslessTests.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,54 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "PrecompiledHeadersUnitTests.h" +#include "gtest/gtest.h" + +#include "../OrthancServer/Internals/DicomImageDecoder.h" + +#if ORTHANC_JPEG_LOSSLESS_ENABLED == 1 + +#include <dcmtk/dcmdata/dcfilefo.h> + +#include "../OrthancServer/ParsedDicomFile.h" +#include "../Core/OrthancException.h" +#include "../Core/ImageFormats/ImageBuffer.h" +#include "../Core/ImageFormats/PngWriter.h" + +using namespace Orthanc; + + + +// TODO Write a test + + +#endif
--- a/UnitTestsSources/Lua.cpp Wed Jun 25 15:34:40 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,136 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - **/ - - -#include "PrecompiledHeadersUnitTests.h" -#include "gtest/gtest.h" - -#include "../Core/Lua/LuaFunctionCall.h" - - -TEST(Lua, Json) -{ - Orthanc::LuaContext lua; - lua.Execute(Orthanc::EmbeddedResources::LUA_TOOLBOX); - lua.Execute("a={}"); - lua.Execute("a['x'] = 10"); - lua.Execute("a['y'] = {}"); - lua.Execute("a['y'][1] = 20"); - lua.Execute("a['y'][2] = 20"); - lua.Execute("PrintRecursive(a)"); - - lua.Execute("function f(a) print(a.bool) return a.bool,20,30,40,50,60 end"); - - Json::Value v, vv, o; - //v["a"] = "b"; - v.append("hello"); - v.append("world"); - v.append("42"); - vv.append("sub"); - vv.append("set"); - v.append(vv); - o = Json::objectValue; - o["x"] = 10; - o["y"] = 20; - o["z"] = 20.5f; - v.append(o); - - { - Orthanc::LuaFunctionCall f(lua, "PrintRecursive"); - f.PushJSON(v); - f.Execute(); - } - - { - Orthanc::LuaFunctionCall f(lua, "f"); - f.PushJSON(o); - ASSERT_THROW(f.ExecutePredicate(), Orthanc::LuaException); - } - - o["bool"] = false; - - { - Orthanc::LuaFunctionCall f(lua, "f"); - f.PushJSON(o); - ASSERT_FALSE(f.ExecutePredicate()); - } - - o["bool"] = true; - - { - Orthanc::LuaFunctionCall f(lua, "f"); - f.PushJSON(o); - ASSERT_TRUE(f.ExecutePredicate()); - } -} - - -TEST(Lua, Existing) -{ - Orthanc::LuaContext lua; - lua.Execute("a={}"); - lua.Execute("function f() end"); - - ASSERT_TRUE(lua.IsExistingFunction("f")); - ASSERT_FALSE(lua.IsExistingFunction("a")); - ASSERT_FALSE(lua.IsExistingFunction("Dummy")); -} - - -TEST(Lua, Simple) -{ - Orthanc::LuaContext lua; - lua.Execute(Orthanc::EmbeddedResources::LUA_TOOLBOX); - - { - Orthanc::LuaFunctionCall f(lua, "PrintRecursive"); - f.PushString("hello"); - f.Execute(); - } - - { - Orthanc::LuaFunctionCall f(lua, "PrintRecursive"); - f.PushBoolean(true); - f.Execute(); - } - - { - Orthanc::LuaFunctionCall f(lua, "PrintRecursive"); - f.PushInteger(42); - f.Execute(); - } - - { - Orthanc::LuaFunctionCall f(lua, "PrintRecursive"); - f.PushDouble(3.1415); - f.Execute(); - } -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/UnitTestsSources/LuaTests.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,290 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "PrecompiledHeadersUnitTests.h" +#include "gtest/gtest.h" + +#include "../Core/Toolbox.h" +#include "../Core/Lua/LuaFunctionCall.h" + +#include <boost/lexical_cast.hpp> + +#if !defined(UNIT_TESTS_WITH_HTTP_CONNEXIONS) +#error "Please set UNIT_TESTS_WITH_HTTP_CONNEXIONS" +#endif + + +TEST(Lua, Json) +{ + Orthanc::LuaContext lua; + lua.Execute(Orthanc::EmbeddedResources::LUA_TOOLBOX); + lua.Execute("a={}"); + lua.Execute("a['x'] = 10"); + lua.Execute("a['y'] = {}"); + lua.Execute("a['y'][1] = 20"); + lua.Execute("a['y'][2] = 20"); + lua.Execute("PrintRecursive(a)"); + + lua.Execute("function f(a) print(a.bool) return a.bool,20,30,40,50,60 end"); + + Json::Value v, vv, o; + //v["a"] = "b"; + v.append("hello"); + v.append("world"); + v.append("42"); + vv.append("sub"); + vv.append("set"); + v.append(vv); + o = Json::objectValue; + o["x"] = 10; + o["y"] = 20; + o["z"] = 20.5f; + v.append(o); + + { + Orthanc::LuaFunctionCall f(lua, "PrintRecursive"); + f.PushJson(v); + f.Execute(); + } + + { + Orthanc::LuaFunctionCall f(lua, "f"); + f.PushJson(o); + ASSERT_THROW(f.ExecutePredicate(), Orthanc::LuaException); + } + + o["bool"] = false; + + { + Orthanc::LuaFunctionCall f(lua, "f"); + f.PushJson(o); + ASSERT_FALSE(f.ExecutePredicate()); + } + + o["bool"] = true; + + { + Orthanc::LuaFunctionCall f(lua, "f"); + f.PushJson(o); + ASSERT_TRUE(f.ExecutePredicate()); + } +} + + +TEST(Lua, Existing) +{ + Orthanc::LuaContext lua; + lua.Execute("a={}"); + lua.Execute("function f() end"); + + ASSERT_TRUE(lua.IsExistingFunction("f")); + ASSERT_FALSE(lua.IsExistingFunction("a")); + ASSERT_FALSE(lua.IsExistingFunction("Dummy")); +} + + +TEST(Lua, Simple) +{ + Orthanc::LuaContext lua; + lua.Execute(Orthanc::EmbeddedResources::LUA_TOOLBOX); + + { + Orthanc::LuaFunctionCall f(lua, "PrintRecursive"); + f.PushString("hello"); + f.Execute(); + } + + { + Orthanc::LuaFunctionCall f(lua, "PrintRecursive"); + f.PushBoolean(true); + f.Execute(); + } + + { + Orthanc::LuaFunctionCall f(lua, "PrintRecursive"); + f.PushInteger(42); + f.Execute(); + } + + { + Orthanc::LuaFunctionCall f(lua, "PrintRecursive"); + f.PushDouble(3.1415); + f.Execute(); + } +} + + +TEST(Lua, ReturnJson) +{ + Json::Value b = Json::objectValue; + b["a"] = 42; + b["b"] = 44; + b["c"] = 43; + + Json::Value c = Json::arrayValue; + c.append("test3"); + c.append("test1"); + c.append("test2"); + + Json::Value a = Json::objectValue; + a["Hello"] = "World"; + a["List"] = Json::arrayValue; + a["List"].append(b); + a["List"].append(c); + + Orthanc::LuaContext lua; + + // This is the identity function (it simply returns its input) + lua.Execute("function identity(a) return a end"); + + { + Orthanc::LuaFunctionCall f(lua, "identity"); + f.PushJson("hello"); + Json::Value v; + f.ExecuteToJson(v); + ASSERT_EQ("hello", v.asString()); + } + + { + Orthanc::LuaFunctionCall f(lua, "identity"); + f.PushJson(42.25); + Json::Value v; + f.ExecuteToJson(v); + ASSERT_FLOAT_EQ(42.25f, v.asFloat()); + } + + { + Orthanc::LuaFunctionCall f(lua, "identity"); + Json::Value vv = Json::arrayValue; + f.PushJson(vv); + Json::Value v; + f.ExecuteToJson(v); + ASSERT_EQ(Json::arrayValue, v.type()); + } + + { + Orthanc::LuaFunctionCall f(lua, "identity"); + Json::Value vv = Json::objectValue; + f.PushJson(vv); + Json::Value v; + f.ExecuteToJson(v); + // Lua does not make the distinction between empty lists and empty objects + ASSERT_EQ(Json::arrayValue, v.type()); + } + + { + Orthanc::LuaFunctionCall f(lua, "identity"); + f.PushJson(b); + Json::Value v; + f.ExecuteToJson(v); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_FLOAT_EQ(42.0f, v["a"].asFloat()); + ASSERT_FLOAT_EQ(44.0f, v["b"].asFloat()); + ASSERT_FLOAT_EQ(43.0f, v["c"].asFloat()); + } + + { + Orthanc::LuaFunctionCall f(lua, "identity"); + f.PushJson(c); + Json::Value v; + f.ExecuteToJson(v); + ASSERT_EQ(Json::arrayValue, v.type()); + ASSERT_EQ("test3", v[0].asString()); + ASSERT_EQ("test1", v[1].asString()); + ASSERT_EQ("test2", v[2].asString()); + } + + { + Orthanc::LuaFunctionCall f(lua, "identity"); + f.PushJson(a); + Json::Value v; + f.ExecuteToJson(v); + ASSERT_EQ("World", v["Hello"].asString()); + ASSERT_EQ(42, v["List"][0]["a"].asInt()); + ASSERT_EQ(44, v["List"][0]["b"].asInt()); + ASSERT_EQ(43, v["List"][0]["c"].asInt()); + ASSERT_EQ("test3", v["List"][1][0].asString()); + ASSERT_EQ("test1", v["List"][1][1].asString()); + ASSERT_EQ("test2", v["List"][1][2].asString()); + } +} + +TEST(Lua, Http) +{ + Orthanc::LuaContext lua; + +#if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1 + lua.Execute("JSON = loadstring(HttpGet('http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/JSON.lua')) ()"); + const std::string url("http://orthanc.googlecode.com/hg/OrthancCppClient/SharedLibrary/Product.json"); +#endif + + std::string s; + lua.Execute(s, "print(HttpGet({}))"); + ASSERT_EQ("ERROR", Orthanc::Toolbox::StripSpaces(s)); + +#if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1 + lua.Execute(s, "print(string.len(HttpGet(\"" + url + "\")))"); + ASSERT_LE(100, boost::lexical_cast<int>(Orthanc::Toolbox::StripSpaces(s))); + + // Parse a JSON file + lua.Execute(s, "print(JSON:decode(HttpGet(\"" + url + "\")) ['Product'])"); + ASSERT_EQ("OrthancClient", Orthanc::Toolbox::StripSpaces(s)); + +#if 0 + // This part of the test can only be executed if one instance of + // Orthanc is running on the localhost + + lua.Execute("modality = {}"); + lua.Execute("table.insert(modality, 'ORTHANC')"); + lua.Execute("table.insert(modality, 'localhost')"); + lua.Execute("table.insert(modality, 4242)"); + + lua.Execute(s, "print(HttpPost(\"http://localhost:8042/tools/execute-script\", \"print('hello world')\"))"); + ASSERT_EQ("hello world", Orthanc::Toolbox::StripSpaces(s)); + + lua.Execute(s, "print(JSON:decode(HttpPost(\"http://localhost:8042/tools/execute-script\", \"print('[10,42,1000]')\")) [2])"); + ASSERT_EQ("42", Orthanc::Toolbox::StripSpaces(s)); + + // Add/remove a modality with Lua + Json::Value v; + lua.Execute(s, "print(HttpGet('http://localhost:8042/modalities/lua'))"); + ASSERT_EQ(0, Orthanc::Toolbox::StripSpaces(s).size()); + lua.Execute(s, "print(HttpPut('http://localhost:8042/modalities/lua', JSON:encode(modality)))"); + lua.Execute(v, "print(HttpGet('http://localhost:8042/modalities/lua'))"); + ASSERT_TRUE(v.type() == Json::arrayValue); + lua.Execute(s, "print(HttpDelete('http://localhost:8042/modalities/lua'))"); + lua.Execute(s, "print(HttpGet('http://localhost:8042/modalities/lua'))"); + ASSERT_EQ(0, Orthanc::Toolbox::StripSpaces(s).size()); +#endif + +#endif +}
--- a/UnitTestsSources/MemoryCache.cpp Wed Jun 25 15:34:40 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,230 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - **/ - - -#include "PrecompiledHeadersUnitTests.h" -#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(LRU, Basic) -{ - Orthanc::LeastRecentlyUsedIndex<std::string> r; - - r.Add("d"); - r.Add("a"); - r.Add("c"); - r.Add("b"); - - r.MakeMostRecent("a"); - r.MakeMostRecent("d"); - r.MakeMostRecent("b"); - r.MakeMostRecent("c"); - r.MakeMostRecent("d"); - r.MakeMostRecent("c"); - - ASSERT_EQ("a", r.GetOldest()); - ASSERT_EQ("a", r.RemoveOldest()); - ASSERT_EQ("b", r.GetOldest()); - ASSERT_EQ("b", r.RemoveOldest()); - ASSERT_EQ("d", r.GetOldest()); - ASSERT_EQ("d", r.RemoveOldest()); - ASSERT_EQ("c", r.GetOldest()); - ASSERT_EQ("c", r.RemoveOldest()); - - ASSERT_TRUE(r.IsEmpty()); - - ASSERT_THROW(r.GetOldest(), Orthanc::OrthancException); - ASSERT_THROW(r.RemoveOldest(), Orthanc::OrthancException); -} - - -TEST(LRU, Payload) -{ - Orthanc::LeastRecentlyUsedIndex<std::string, int> r; - - r.Add("a", 420); - r.Add("b", 421); - r.Add("c", 422); - r.Add("d", 423); - - r.MakeMostRecent("a"); - r.MakeMostRecent("d"); - r.MakeMostRecent("b"); - r.MakeMostRecent("c"); - r.MakeMostRecent("d"); - r.MakeMostRecent("c"); - - ASSERT_TRUE(r.Contains("b")); - ASSERT_EQ(421, r.Invalidate("b")); - 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.GetOldest()); - ASSERT_EQ(420, r.GetOldestPayload()); - ASSERT_EQ("a", r.RemoveOldest(p)); ASSERT_EQ(420, p); - - ASSERT_EQ("d", r.GetOldest()); - ASSERT_EQ(423, r.GetOldestPayload()); - ASSERT_EQ("d", r.RemoveOldest(p)); ASSERT_EQ(423, p); - - ASSERT_EQ("c", r.GetOldest()); - ASSERT_EQ(422, r.GetOldestPayload()); - ASSERT_EQ("c", r.RemoveOldest(p)); ASSERT_EQ(422, p); - - ASSERT_TRUE(r.IsEmpty()); -} - - -TEST(LRU, PayloadUpdate) -{ - Orthanc::LeastRecentlyUsedIndex<std::string, int> r; - - r.Add("a", 420); - r.Add("b", 421); - r.Add("d", 423); - - r.MakeMostRecent("a", 424); - r.MakeMostRecent("d", 421); - - ASSERT_EQ("b", r.GetOldest()); - ASSERT_EQ(421, r.GetOldestPayload()); - r.RemoveOldest(); - - ASSERT_EQ("a", r.GetOldest()); - ASSERT_EQ(424, r.GetOldestPayload()); - r.RemoveOldest(); - - ASSERT_EQ("d", r.GetOldest()); - ASSERT_EQ(421, r.GetOldestPayload()); - r.RemoveOldest(); - - ASSERT_TRUE(r.IsEmpty()); -} - - - -TEST(LRU, PayloadUpdateBis) -{ - Orthanc::LeastRecentlyUsedIndex<std::string, int> r; - - r.AddOrMakeMostRecent("a", 420); - r.AddOrMakeMostRecent("b", 421); - r.AddOrMakeMostRecent("d", 423); - r.AddOrMakeMostRecent("a", 424); - r.AddOrMakeMostRecent("d", 421); - - ASSERT_EQ("b", r.GetOldest()); - ASSERT_EQ(421, r.GetOldestPayload()); - r.RemoveOldest(); - - ASSERT_EQ("a", r.GetOldest()); - ASSERT_EQ(424, r.GetOldestPayload()); - r.RemoveOldest(); - - ASSERT_EQ("d", r.GetOldest()); - ASSERT_EQ(421, r.GetOldestPayload()); - r.RemoveOldest(); - - ASSERT_TRUE(r.IsEmpty()); -} - - - - -namespace -{ - 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/UnitTestsSources/MemoryCacheTests.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,230 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "PrecompiledHeadersUnitTests.h" +#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(LRU, Basic) +{ + Orthanc::LeastRecentlyUsedIndex<std::string> r; + + r.Add("d"); + r.Add("a"); + r.Add("c"); + r.Add("b"); + + r.MakeMostRecent("a"); + r.MakeMostRecent("d"); + r.MakeMostRecent("b"); + r.MakeMostRecent("c"); + r.MakeMostRecent("d"); + r.MakeMostRecent("c"); + + ASSERT_EQ("a", r.GetOldest()); + ASSERT_EQ("a", r.RemoveOldest()); + ASSERT_EQ("b", r.GetOldest()); + ASSERT_EQ("b", r.RemoveOldest()); + ASSERT_EQ("d", r.GetOldest()); + ASSERT_EQ("d", r.RemoveOldest()); + ASSERT_EQ("c", r.GetOldest()); + ASSERT_EQ("c", r.RemoveOldest()); + + ASSERT_TRUE(r.IsEmpty()); + + ASSERT_THROW(r.GetOldest(), Orthanc::OrthancException); + ASSERT_THROW(r.RemoveOldest(), Orthanc::OrthancException); +} + + +TEST(LRU, Payload) +{ + Orthanc::LeastRecentlyUsedIndex<std::string, int> r; + + r.Add("a", 420); + r.Add("b", 421); + r.Add("c", 422); + r.Add("d", 423); + + r.MakeMostRecent("a"); + r.MakeMostRecent("d"); + r.MakeMostRecent("b"); + r.MakeMostRecent("c"); + r.MakeMostRecent("d"); + r.MakeMostRecent("c"); + + ASSERT_TRUE(r.Contains("b")); + ASSERT_EQ(421, r.Invalidate("b")); + 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.GetOldest()); + ASSERT_EQ(420, r.GetOldestPayload()); + ASSERT_EQ("a", r.RemoveOldest(p)); ASSERT_EQ(420, p); + + ASSERT_EQ("d", r.GetOldest()); + ASSERT_EQ(423, r.GetOldestPayload()); + ASSERT_EQ("d", r.RemoveOldest(p)); ASSERT_EQ(423, p); + + ASSERT_EQ("c", r.GetOldest()); + ASSERT_EQ(422, r.GetOldestPayload()); + ASSERT_EQ("c", r.RemoveOldest(p)); ASSERT_EQ(422, p); + + ASSERT_TRUE(r.IsEmpty()); +} + + +TEST(LRU, PayloadUpdate) +{ + Orthanc::LeastRecentlyUsedIndex<std::string, int> r; + + r.Add("a", 420); + r.Add("b", 421); + r.Add("d", 423); + + r.MakeMostRecent("a", 424); + r.MakeMostRecent("d", 421); + + ASSERT_EQ("b", r.GetOldest()); + ASSERT_EQ(421, r.GetOldestPayload()); + r.RemoveOldest(); + + ASSERT_EQ("a", r.GetOldest()); + ASSERT_EQ(424, r.GetOldestPayload()); + r.RemoveOldest(); + + ASSERT_EQ("d", r.GetOldest()); + ASSERT_EQ(421, r.GetOldestPayload()); + r.RemoveOldest(); + + ASSERT_TRUE(r.IsEmpty()); +} + + + +TEST(LRU, PayloadUpdateBis) +{ + Orthanc::LeastRecentlyUsedIndex<std::string, int> r; + + r.AddOrMakeMostRecent("a", 420); + r.AddOrMakeMostRecent("b", 421); + r.AddOrMakeMostRecent("d", 423); + r.AddOrMakeMostRecent("a", 424); + r.AddOrMakeMostRecent("d", 421); + + ASSERT_EQ("b", r.GetOldest()); + ASSERT_EQ(421, r.GetOldestPayload()); + r.RemoveOldest(); + + ASSERT_EQ("a", r.GetOldest()); + ASSERT_EQ(424, r.GetOldestPayload()); + r.RemoveOldest(); + + ASSERT_EQ("d", r.GetOldest()); + ASSERT_EQ(421, r.GetOldestPayload()); + r.RemoveOldest(); + + ASSERT_TRUE(r.IsEmpty()); +} + + + + +namespace +{ + class Integer : public Orthanc::IDynamicObject + { + private: + std::string& log_; + int value_; + + public: + Integer(std::string& log, int v) : log_(log), value_(v) + { + } + + virtual ~Integer() + { + LOG(INFO) << "Removing cache entry for " << value_; + log_ += boost::lexical_cast<std::string>(value_) + " "; + } + + int GetValue() const + { + return value_; + } + }; + + class IntegerProvider : public Orthanc::ICachePageProvider + { + public: + std::string log_; + + Orthanc::IDynamicObject* Provide(const std::string& s) + { + LOG(INFO) << "Providing " << s; + return new Integer(log_, boost::lexical_cast<int>(s)); + } + }; +} + + +TEST(MemoryCache, Basic) +{ + IntegerProvider provider; + + { + Orthanc::MemoryCache cache(provider, 3); + cache.Access("42"); // 42 -> exit + cache.Access("43"); // 43, 42 -> exit + cache.Access("45"); // 45, 43, 42 -> exit + cache.Access("42"); // 42, 45, 43 -> exit + cache.Access("43"); // 43, 42, 45 -> exit + cache.Access("47"); // 45 is removed; 47, 43, 42 -> exit + cache.Access("44"); // 42 is removed; 44, 47, 43 -> exit + cache.Access("42"); // 43 is removed; 42, 44, 47 -> exit + // Closing the cache: 47, 44, 42 are successively removed + } + + ASSERT_EQ("45 42 43 47 44 42 ", provider.log_); +}
--- a/UnitTestsSources/MultiThreading.cpp Wed Jun 25 15:34:40 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,276 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - **/ - - -#include "PrecompiledHeadersUnitTests.h" -#include "gtest/gtest.h" - -#include <glog/logging.h> - -#include "../Core/OrthancException.h" -#include "../Core/Toolbox.h" -#include "../Core/MultiThreading/ArrayFilledByThreads.h" -#include "../Core/MultiThreading/Locker.h" -#include "../Core/MultiThreading/Mutex.h" -#include "../Core/MultiThreading/ReaderWriterLock.h" -#include "../Core/MultiThreading/ThreadedCommandProcessor.h" - -using namespace Orthanc; - -namespace -{ - class DynamicInteger : public ICommand - { - private: - int value_; - std::set<int>& target_; - - public: - DynamicInteger(int value, std::set<int>& target) : - value_(value), target_(target) - { - } - - int GetValue() const - { - return value_; - } - - virtual bool Execute() - { - static boost::mutex mutex; - boost::mutex::scoped_lock lock(mutex); - target_.insert(value_); - return true; - } - }; - - class MyFiller : public ArrayFilledByThreads::IFiller - { - private: - int size_; - unsigned int created_; - std::set<int> set_; - - public: - MyFiller(int size) : size_(size), created_(0) - { - } - - virtual size_t GetFillerSize() - { - return size_; - } - - virtual IDynamicObject* GetFillerItem(size_t index) - { - static boost::mutex mutex; - boost::mutex::scoped_lock lock(mutex); - created_++; - return new DynamicInteger(index * 2, set_); - } - - unsigned int GetCreatedCount() const - { - return created_; - } - - std::set<int> GetSet() - { - return set_; - } - }; -} - - - - -TEST(MultiThreading, SharedMessageQueueBasic) -{ - std::set<int> s; - - SharedMessageQueue q; - ASSERT_TRUE(q.WaitEmpty(0)); - q.Enqueue(new DynamicInteger(10, s)); - ASSERT_FALSE(q.WaitEmpty(1)); - q.Enqueue(new DynamicInteger(20, s)); - q.Enqueue(new DynamicInteger(30, s)); - q.Enqueue(new DynamicInteger(40, s)); - - std::auto_ptr<DynamicInteger> i; - i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(10, i->GetValue()); - i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(20, i->GetValue()); - i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(30, i->GetValue()); - ASSERT_FALSE(q.WaitEmpty(1)); - i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(40, i->GetValue()); - ASSERT_TRUE(q.WaitEmpty(0)); - ASSERT_EQ(NULL, q.Dequeue(1)); -} - - -TEST(MultiThreading, SharedMessageQueueClean) -{ - std::set<int> s; - - try - { - SharedMessageQueue q; - q.Enqueue(new DynamicInteger(10, s)); - q.Enqueue(new DynamicInteger(20, s)); - throw OrthancException("Nope"); - } - catch (OrthancException&) - { - } -} - - -TEST(MultiThreading, ArrayFilledByThreadEmpty) -{ - MyFiller f(0); - ArrayFilledByThreads a(f); - a.SetThreadCount(1); - ASSERT_EQ(0, a.GetSize()); -} - - -TEST(MultiThreading, ArrayFilledByThread1) -{ - MyFiller f(100); - ArrayFilledByThreads a(f); - a.SetThreadCount(1); - ASSERT_EQ(100, a.GetSize()); - for (size_t i = 0; i < a.GetSize(); i++) - { - ASSERT_EQ(2 * i, dynamic_cast<DynamicInteger&>(a.GetItem(i)).GetValue()); - } -} - - -TEST(MultiThreading, ArrayFilledByThread4) -{ - MyFiller f(100); - ArrayFilledByThreads a(f); - a.SetThreadCount(4); - ASSERT_EQ(100, a.GetSize()); - for (size_t i = 0; i < a.GetSize(); i++) - { - ASSERT_EQ(2 * i, dynamic_cast<DynamicInteger&>(a.GetItem(i)).GetValue()); - } - - ASSERT_EQ(100u, f.GetCreatedCount()); - - a.Invalidate(); - - ASSERT_EQ(100, a.GetSize()); - ASSERT_EQ(200u, f.GetCreatedCount()); - ASSERT_EQ(4u, a.GetThreadCount()); - ASSERT_TRUE(f.GetSet().empty()); - - for (size_t i = 0; i < a.GetSize(); i++) - { - ASSERT_EQ(2 * i, dynamic_cast<DynamicInteger&>(a.GetItem(i)).GetValue()); - } -} - - -TEST(MultiThreading, CommandProcessor) -{ - ThreadedCommandProcessor p(4); - - std::set<int> s; - - for (size_t i = 0; i < 100; i++) - { - p.Post(new DynamicInteger(i * 2, s)); - } - - p.Join(); - - for (size_t i = 0; i < 200; i++) - { - if (i % 2) - ASSERT_TRUE(s.find(i) == s.end()); - else - ASSERT_TRUE(s.find(i) != s.end()); - } -} - - -TEST(MultiThreading, Mutex) -{ - Mutex mutex; - Locker locker(mutex); -} - - -TEST(MultiThreading, ReaderWriterLock) -{ - ReaderWriterLock lock; - - { - Locker locker1(lock.ForReader()); - Locker locker2(lock.ForReader()); - } - - { - Locker locker3(lock.ForWriter()); - } -} - - - - - -#include "../OrthancServer/DicomProtocol/ReusableDicomUserConnection.h" - -TEST(ReusableDicomUserConnection, DISABLED_Basic) -{ - ReusableDicomUserConnection c; - c.SetMillisecondsBeforeClose(200); - printf("START\n"); fflush(stdout); - { - ReusableDicomUserConnection::Locker lock(c, "STORESCP", "localhost", 2000, ModalityManufacturer_Generic); - lock.GetConnection().StoreFile("/home/jodogne/DICOM/Cardiac/MR.X.1.2.276.0.7230010.3.1.4.2831157719.2256.1336386844.676281"); - } - - printf("**\n"); fflush(stdout); - Toolbox::USleep(1000000); - printf("**\n"); fflush(stdout); - - { - ReusableDicomUserConnection::Locker lock(c, "STORESCP", "localhost", 2000, ModalityManufacturer_Generic); - lock.GetConnection().StoreFile("/home/jodogne/DICOM/Cardiac/MR.X.1.2.276.0.7230010.3.1.4.2831157719.2256.1336386844.676277"); - } - - Toolbox::ServerBarrier(); - printf("DONE\n"); fflush(stdout); -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/UnitTestsSources/MultiThreadingTests.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,374 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "PrecompiledHeadersUnitTests.h" +#include "gtest/gtest.h" +#include <glog/logging.h> + +#include "../OrthancServer/Scheduler/ServerScheduler.h" + +#include "../Core/OrthancException.h" +#include "../Core/Toolbox.h" +#include "../Core/MultiThreading/ArrayFilledByThreads.h" +#include "../Core/MultiThreading/Locker.h" +#include "../Core/MultiThreading/Mutex.h" +#include "../Core/MultiThreading/ReaderWriterLock.h" +#include "../Core/MultiThreading/ThreadedCommandProcessor.h" + +using namespace Orthanc; + +namespace +{ + class DynamicInteger : public ICommand + { + private: + int value_; + std::set<int>& target_; + + public: + DynamicInteger(int value, std::set<int>& target) : + value_(value), target_(target) + { + } + + int GetValue() const + { + return value_; + } + + virtual bool Execute() + { + static boost::mutex mutex; + boost::mutex::scoped_lock lock(mutex); + target_.insert(value_); + return true; + } + }; + + class MyFiller : public ArrayFilledByThreads::IFiller + { + private: + int size_; + unsigned int created_; + std::set<int> set_; + + public: + MyFiller(int size) : size_(size), created_(0) + { + } + + virtual size_t GetFillerSize() + { + return size_; + } + + virtual IDynamicObject* GetFillerItem(size_t index) + { + static boost::mutex mutex; + boost::mutex::scoped_lock lock(mutex); + created_++; + return new DynamicInteger(index * 2, set_); + } + + unsigned int GetCreatedCount() const + { + return created_; + } + + std::set<int> GetSet() + { + return set_; + } + }; +} + + + + +TEST(MultiThreading, SharedMessageQueueBasic) +{ + std::set<int> s; + + SharedMessageQueue q; + ASSERT_TRUE(q.WaitEmpty(0)); + q.Enqueue(new DynamicInteger(10, s)); + ASSERT_FALSE(q.WaitEmpty(1)); + q.Enqueue(new DynamicInteger(20, s)); + q.Enqueue(new DynamicInteger(30, s)); + q.Enqueue(new DynamicInteger(40, s)); + + std::auto_ptr<DynamicInteger> i; + i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(10, i->GetValue()); + i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(20, i->GetValue()); + i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(30, i->GetValue()); + ASSERT_FALSE(q.WaitEmpty(1)); + i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(40, i->GetValue()); + ASSERT_TRUE(q.WaitEmpty(0)); + ASSERT_EQ(NULL, q.Dequeue(1)); +} + + +TEST(MultiThreading, SharedMessageQueueClean) +{ + std::set<int> s; + + try + { + SharedMessageQueue q; + q.Enqueue(new DynamicInteger(10, s)); + q.Enqueue(new DynamicInteger(20, s)); + throw OrthancException("Nope"); + } + catch (OrthancException&) + { + } +} + + +TEST(MultiThreading, ArrayFilledByThreadEmpty) +{ + MyFiller f(0); + ArrayFilledByThreads a(f); + a.SetThreadCount(1); + ASSERT_EQ(0, a.GetSize()); +} + + +TEST(MultiThreading, ArrayFilledByThread1) +{ + MyFiller f(100); + ArrayFilledByThreads a(f); + a.SetThreadCount(1); + ASSERT_EQ(100, a.GetSize()); + for (size_t i = 0; i < a.GetSize(); i++) + { + ASSERT_EQ(2 * i, dynamic_cast<DynamicInteger&>(a.GetItem(i)).GetValue()); + } +} + + +TEST(MultiThreading, ArrayFilledByThread4) +{ + MyFiller f(100); + ArrayFilledByThreads a(f); + a.SetThreadCount(4); + ASSERT_EQ(100, a.GetSize()); + for (size_t i = 0; i < a.GetSize(); i++) + { + ASSERT_EQ(2 * i, dynamic_cast<DynamicInteger&>(a.GetItem(i)).GetValue()); + } + + ASSERT_EQ(100u, f.GetCreatedCount()); + + a.Invalidate(); + + ASSERT_EQ(100, a.GetSize()); + ASSERT_EQ(200u, f.GetCreatedCount()); + ASSERT_EQ(4u, a.GetThreadCount()); + ASSERT_TRUE(f.GetSet().empty()); + + for (size_t i = 0; i < a.GetSize(); i++) + { + ASSERT_EQ(2 * i, dynamic_cast<DynamicInteger&>(a.GetItem(i)).GetValue()); + } +} + + +TEST(MultiThreading, CommandProcessor) +{ + ThreadedCommandProcessor p(4); + + std::set<int> s; + + for (size_t i = 0; i < 100; i++) + { + p.Post(new DynamicInteger(i * 2, s)); + } + + p.Join(); + + for (size_t i = 0; i < 200; i++) + { + if (i % 2) + ASSERT_TRUE(s.find(i) == s.end()); + else + ASSERT_TRUE(s.find(i) != s.end()); + } +} + + +TEST(MultiThreading, Mutex) +{ + Mutex mutex; + Locker locker(mutex); +} + + +TEST(MultiThreading, ReaderWriterLock) +{ + ReaderWriterLock lock; + + { + Locker locker1(lock.ForReader()); + Locker locker2(lock.ForReader()); + } + + { + Locker locker3(lock.ForWriter()); + } +} + + + +#include "../OrthancServer/DicomProtocol/ReusableDicomUserConnection.h" + +TEST(ReusableDicomUserConnection, DISABLED_Basic) +{ + ReusableDicomUserConnection c; + c.SetMillisecondsBeforeClose(200); + printf("START\n"); fflush(stdout); + + { + ReusableDicomUserConnection::Locker lock(c, "STORESCP", "localhost", 2000, ModalityManufacturer_Generic); + lock.GetConnection().StoreFile("/home/jodogne/DICOM/Cardiac/MR.X.1.2.276.0.7230010.3.1.4.2831157719.2256.1336386844.676281"); + } + + printf("**\n"); fflush(stdout); + Toolbox::USleep(1000000); + printf("**\n"); fflush(stdout); + + { + ReusableDicomUserConnection::Locker lock(c, "STORESCP", "localhost", 2000, ModalityManufacturer_Generic); + lock.GetConnection().StoreFile("/home/jodogne/DICOM/Cardiac/MR.X.1.2.276.0.7230010.3.1.4.2831157719.2256.1336386844.676277"); + } + + Toolbox::ServerBarrier(); + printf("DONE\n"); fflush(stdout); +} + + + +class Tutu : public IServerCommand +{ +private: + int factor_; + +public: + Tutu(int f) : factor_(f) + { + } + + virtual bool Apply(ListOfStrings& outputs, + const ListOfStrings& inputs) + { + for (ListOfStrings::const_iterator + it = inputs.begin(); it != inputs.end(); ++it) + { + int a = boost::lexical_cast<int>(*it); + int b = factor_ * a; + + printf("%d * %d = %d\n", a, factor_, b); + + //if (a == 84) { printf("BREAK\n"); return false; } + + outputs.push_back(boost::lexical_cast<std::string>(b)); + } + + Toolbox::USleep(100000); + + return true; + } +}; + + +static void Tata(ServerScheduler* s, ServerJob* j, bool* done) +{ + typedef IServerCommand::ListOfStrings ListOfStrings; + + while (!(*done)) + { + ListOfStrings l; + s->GetListOfJobs(l); + for (ListOfStrings::iterator it = l.begin(); it != l.end(); ++it) + { + printf(">> %s: %0.1f\n", it->c_str(), 100.0f * s->GetProgress(*it)); + } + Toolbox::USleep(10000); + } +} + + +TEST(MultiThreading, ServerScheduler) +{ + ServerScheduler scheduler(10); + + ServerJob job; + ServerCommandInstance& f2 = job.AddCommand(new Tutu(2)); + ServerCommandInstance& f3 = job.AddCommand(new Tutu(3)); + ServerCommandInstance& f4 = job.AddCommand(new Tutu(4)); + ServerCommandInstance& f5 = job.AddCommand(new Tutu(5)); + f2.AddInput(boost::lexical_cast<std::string>(42)); + //f3.AddInput(boost::lexical_cast<std::string>(42)); + //f4.AddInput(boost::lexical_cast<std::string>(42)); + f2.ConnectOutput(f3); + f3.ConnectOutput(f4); + f4.ConnectOutput(f5); + + f3.SetConnectedToSink(true); + f5.SetConnectedToSink(true); + + job.SetDescription("tutu"); + + bool done = false; + boost::thread t(Tata, &scheduler, &job, &done); + + + //scheduler.Submit(job); + + IServerCommand::ListOfStrings l; + scheduler.SubmitAndWait(l, job); + + ASSERT_EQ(2, l.size()); + ASSERT_EQ(42 * 2 * 3, boost::lexical_cast<int>(l.front())); + ASSERT_EQ(42 * 2 * 3 * 4 * 5, boost::lexical_cast<int>(l.back())); + + for (IServerCommand::ListOfStrings::iterator i = l.begin(); i != l.end(); i++) + { + printf("** %s\n", i->c_str()); + } + + //Toolbox::ServerBarrier(); + //Toolbox::USleep(3000000); + + done = true; + t.join(); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/UnitTestsSources/PluginsTests.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,78 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "PrecompiledHeadersUnitTests.h" +#include "gtest/gtest.h" + +#include <glog/logging.h> + +#include "../Plugins/Engine/PluginsManager.h" + +using namespace Orthanc; + +TEST(SharedLibrary, Basic) +{ +#if defined(_WIN32) + SharedLibrary l("kernel32.dll"); + ASSERT_THROW(l.GetFunction("world"), OrthancException); + ASSERT_TRUE(l.GetFunction("GetVersionExW") != NULL); + ASSERT_TRUE(l.HasFunction("GetVersionExW")); + ASSERT_FALSE(l.HasFunction("world")); + +#elif defined(__linux) || defined(__FreeBSD_kernel__) + SharedLibrary l("libdl.so"); + ASSERT_THROW(l.GetFunction("world"), OrthancException); + ASSERT_TRUE(l.GetFunction("dlopen") != NULL); + ASSERT_TRUE(l.HasFunction("dlclose")); + ASSERT_FALSE(l.HasFunction("world")); + +#elif defined(__FreeBSD__) + // dlopen() in FreeBSD is supplied by libc, libc.so is + // a ldscript, so we can't actually use it. Use thread + // library instead - if it works - dlopen() is good. + SharedLibrary l("libpthread.so"); + ASSERT_THROW(l.GetFunction("world"), OrthancException); + ASSERT_TRUE(l.GetFunction("pthread_create") != NULL); + ASSERT_TRUE(l.HasFunction("pthread_cancel")); + ASSERT_FALSE(l.HasFunction("world")); + +#elif defined(__APPLE__) && defined(__MACH__) + SharedLibrary l("libdl.dylib"); + ASSERT_THROW(l.GetFunction("world"), OrthancException); + ASSERT_TRUE(l.GetFunction("dlopen") != NULL); + ASSERT_TRUE(l.HasFunction("dlclose")); + ASSERT_FALSE(l.HasFunction("world")); + +#else +#error Support your platform here +#endif +}
--- a/UnitTestsSources/Png.cpp Wed Jun 25 15:34:40 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,186 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - **/ - - -#include "PrecompiledHeadersUnitTests.h" -#include "gtest/gtest.h" - -#include <stdint.h> -#include "../Core/ImageFormats/PngReader.h" -#include "../Core/ImageFormats/PngWriter.h" -#include "../Core/Toolbox.h" -#include "../Core/Uuid.h" - - -TEST(PngWriter, ColorPattern) -{ - Orthanc::PngWriter w; - int width = 17; - int height = 61; - int pitch = width * 3; - - std::vector<uint8_t> image(height * pitch); - for (int y = 0; y < height; y++) - { - uint8_t *p = &image[0] + y * pitch; - for (int x = 0; x < width; x++, p += 3) - { - p[0] = (y % 3 == 0) ? 255 : 0; - p[1] = (y % 3 == 1) ? 255 : 0; - p[2] = (y % 3 == 2) ? 255 : 0; - } - } - - w.WriteToFile("UnitTestsResults/ColorPattern.png", width, height, pitch, Orthanc::PixelFormat_RGB24, &image[0]); - - std::string f, md5; - Orthanc::Toolbox::ReadFile(f, "UnitTestsResults/ColorPattern.png"); - Orthanc::Toolbox::ComputeMD5(md5, f); - ASSERT_EQ("604e785f53c99cae6ea4584870b2c41d", md5); -} - -TEST(PngWriter, Gray8Pattern) -{ - Orthanc::PngWriter w; - int width = 17; - int height = 256; - int pitch = width; - - std::vector<uint8_t> image(height * pitch); - for (int y = 0; y < height; y++) - { - uint8_t *p = &image[0] + y * pitch; - for (int x = 0; x < width; x++, p++) - { - *p = y; - } - } - - w.WriteToFile("UnitTestsResults/Gray8Pattern.png", width, height, pitch, Orthanc::PixelFormat_Grayscale8, &image[0]); - - std::string f, md5; - Orthanc::Toolbox::ReadFile(f, "UnitTestsResults/Gray8Pattern.png"); - Orthanc::Toolbox::ComputeMD5(md5, f); - ASSERT_EQ("5a9b98bea3d0a6d983980cc38bfbcdb3", md5); -} - -TEST(PngWriter, Gray16Pattern) -{ - Orthanc::PngWriter w; - int width = 256; - int height = 256; - int pitch = width * 2 + 16; - - std::vector<uint8_t> image(height * pitch); - - int v = 0; - for (int y = 0; y < height; y++) - { - uint16_t *p = reinterpret_cast<uint16_t*>(&image[0] + y * pitch); - for (int x = 0; x < width; x++, p++, v++) - { - *p = v; - } - } - - w.WriteToFile("UnitTestsResults/Gray16Pattern.png", width, height, pitch, Orthanc::PixelFormat_Grayscale16, &image[0]); - - std::string f, md5; - Orthanc::Toolbox::ReadFile(f, "UnitTestsResults/Gray16Pattern.png"); - Orthanc::Toolbox::ComputeMD5(md5, f); - ASSERT_EQ("0785866a08bf0a02d2eeff87f658571c", md5); -} - -TEST(PngWriter, EndToEnd) -{ - Orthanc::PngWriter w; - int width = 256; - int height = 256; - int pitch = width * 2 + 16; - - std::vector<uint8_t> image(height * pitch); - - int v = 0; - for (int y = 0; y < height; y++) - { - uint16_t *p = reinterpret_cast<uint16_t*>(&image[0] + y * pitch); - for (int x = 0; x < width; x++, p++, v++) - { - *p = v; - } - } - - std::string s; - w.WriteToMemory(s, width, height, pitch, Orthanc::PixelFormat_Grayscale16, &image[0]); - - { - Orthanc::PngReader r; - r.ReadFromMemory(s); - - ASSERT_EQ(r.GetFormat(), Orthanc::PixelFormat_Grayscale16); - ASSERT_EQ(r.GetWidth(), width); - ASSERT_EQ(r.GetHeight(), height); - - v = 0; - for (int y = 0; y < height; y++) - { - const uint16_t *p = reinterpret_cast<const uint16_t*>((const uint8_t*) r.GetConstBuffer() + y * r.GetPitch()); - ASSERT_EQ(p, r.GetConstRow(y)); - for (int x = 0; x < width; x++, p++, v++) - { - ASSERT_EQ(*p, v); - } - } - } - - { - Orthanc::Toolbox::TemporaryFile tmp; - Orthanc::Toolbox::WriteFile(s, tmp.GetPath()); - - Orthanc::PngReader r2; - r2.ReadFromFile(tmp.GetPath()); - - ASSERT_EQ(r2.GetFormat(), Orthanc::PixelFormat_Grayscale16); - ASSERT_EQ(r2.GetWidth(), width); - ASSERT_EQ(r2.GetHeight(), height); - - v = 0; - for (int y = 0; y < height; y++) - { - const uint16_t *p = reinterpret_cast<const uint16_t*>((const uint8_t*) r2.GetConstBuffer() + y * r2.GetPitch()); - ASSERT_EQ(p, r2.GetConstRow(y)); - for (int x = 0; x < width; x++, p++, v++) - { - ASSERT_EQ(*p, v); - } - } - } -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/UnitTestsSources/PngTests.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,186 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "PrecompiledHeadersUnitTests.h" +#include "gtest/gtest.h" + +#include <stdint.h> +#include "../Core/ImageFormats/PngReader.h" +#include "../Core/ImageFormats/PngWriter.h" +#include "../Core/Toolbox.h" +#include "../Core/Uuid.h" + + +TEST(PngWriter, ColorPattern) +{ + Orthanc::PngWriter w; + int width = 17; + int height = 61; + int pitch = width * 3; + + std::vector<uint8_t> image(height * pitch); + for (int y = 0; y < height; y++) + { + uint8_t *p = &image[0] + y * pitch; + for (int x = 0; x < width; x++, p += 3) + { + p[0] = (y % 3 == 0) ? 255 : 0; + p[1] = (y % 3 == 1) ? 255 : 0; + p[2] = (y % 3 == 2) ? 255 : 0; + } + } + + w.WriteToFile("UnitTestsResults/ColorPattern.png", width, height, pitch, Orthanc::PixelFormat_RGB24, &image[0]); + + std::string f, md5; + Orthanc::Toolbox::ReadFile(f, "UnitTestsResults/ColorPattern.png"); + Orthanc::Toolbox::ComputeMD5(md5, f); + ASSERT_EQ("604e785f53c99cae6ea4584870b2c41d", md5); +} + +TEST(PngWriter, Gray8Pattern) +{ + Orthanc::PngWriter w; + int width = 17; + int height = 256; + int pitch = width; + + std::vector<uint8_t> image(height * pitch); + for (int y = 0; y < height; y++) + { + uint8_t *p = &image[0] + y * pitch; + for (int x = 0; x < width; x++, p++) + { + *p = y; + } + } + + w.WriteToFile("UnitTestsResults/Gray8Pattern.png", width, height, pitch, Orthanc::PixelFormat_Grayscale8, &image[0]); + + std::string f, md5; + Orthanc::Toolbox::ReadFile(f, "UnitTestsResults/Gray8Pattern.png"); + Orthanc::Toolbox::ComputeMD5(md5, f); + ASSERT_EQ("5a9b98bea3d0a6d983980cc38bfbcdb3", md5); +} + +TEST(PngWriter, Gray16Pattern) +{ + Orthanc::PngWriter w; + int width = 256; + int height = 256; + int pitch = width * 2 + 16; + + std::vector<uint8_t> image(height * pitch); + + int v = 0; + for (int y = 0; y < height; y++) + { + uint16_t *p = reinterpret_cast<uint16_t*>(&image[0] + y * pitch); + for (int x = 0; x < width; x++, p++, v++) + { + *p = v; + } + } + + w.WriteToFile("UnitTestsResults/Gray16Pattern.png", width, height, pitch, Orthanc::PixelFormat_Grayscale16, &image[0]); + + std::string f, md5; + Orthanc::Toolbox::ReadFile(f, "UnitTestsResults/Gray16Pattern.png"); + Orthanc::Toolbox::ComputeMD5(md5, f); + ASSERT_EQ("0785866a08bf0a02d2eeff87f658571c", md5); +} + +TEST(PngWriter, EndToEnd) +{ + Orthanc::PngWriter w; + int width = 256; + int height = 256; + int pitch = width * 2 + 16; + + std::vector<uint8_t> image(height * pitch); + + int v = 0; + for (int y = 0; y < height; y++) + { + uint16_t *p = reinterpret_cast<uint16_t*>(&image[0] + y * pitch); + for (int x = 0; x < width; x++, p++, v++) + { + *p = v; + } + } + + std::string s; + w.WriteToMemory(s, width, height, pitch, Orthanc::PixelFormat_Grayscale16, &image[0]); + + { + Orthanc::PngReader r; + r.ReadFromMemory(s); + + ASSERT_EQ(r.GetFormat(), Orthanc::PixelFormat_Grayscale16); + ASSERT_EQ(r.GetWidth(), width); + ASSERT_EQ(r.GetHeight(), height); + + v = 0; + for (int y = 0; y < height; y++) + { + const uint16_t *p = reinterpret_cast<const uint16_t*>((const uint8_t*) r.GetConstBuffer() + y * r.GetPitch()); + ASSERT_EQ(p, r.GetConstRow(y)); + for (int x = 0; x < width; x++, p++, v++) + { + ASSERT_EQ(*p, v); + } + } + } + + { + Orthanc::Toolbox::TemporaryFile tmp; + Orthanc::Toolbox::WriteFile(s, tmp.GetPath()); + + Orthanc::PngReader r2; + r2.ReadFromFile(tmp.GetPath()); + + ASSERT_EQ(r2.GetFormat(), Orthanc::PixelFormat_Grayscale16); + ASSERT_EQ(r2.GetWidth(), width); + ASSERT_EQ(r2.GetHeight(), height); + + v = 0; + for (int y = 0; y < height; y++) + { + const uint16_t *p = reinterpret_cast<const uint16_t*>((const uint8_t*) r2.GetConstBuffer() + y * r2.GetPitch()); + ASSERT_EQ(p, r2.GetConstRow(y)); + for (int x = 0; x < width; x++, p++, v++) + { + ASSERT_EQ(*p, v); + } + } + } +}
--- a/UnitTestsSources/PrecompiledHeadersUnitTests.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/UnitTestsSources/PrecompiledHeadersUnitTests.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/UnitTestsSources/PrecompiledHeadersUnitTests.h Wed Jun 25 15:34:40 2014 +0200 +++ b/UnitTestsSources/PrecompiledHeadersUnitTests.h Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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
--- a/UnitTestsSources/RestApi.cpp Wed Jun 25 15:34:40 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,161 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - **/ - - -#include "PrecompiledHeadersUnitTests.h" -#include "gtest/gtest.h" - -#include <ctype.h> -#include <glog/logging.h> - -#include "../Core/ChunkedBuffer.h" -#include "../Core/HttpClient.h" -#include "../Core/RestApi/RestApi.h" -#include "../Core/Uuid.h" -#include "../Core/OrthancException.h" -#include "../Core/Compression/ZlibCompressor.h" - -using namespace Orthanc; - -#if !defined(UNIT_TESTS_WITH_HTTP_CONNEXIONS) -#error "Please set UNIT_TESTS_WITH_HTTP_CONNEXIONS" -#endif - -TEST(HttpClient, Basic) -{ - HttpClient c; - ASSERT_FALSE(c.IsVerbose()); - c.SetVerbose(true); - ASSERT_TRUE(c.IsVerbose()); - c.SetVerbose(false); - ASSERT_FALSE(c.IsVerbose()); - -#if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1 - Json::Value v; - c.SetUrl("http://orthanc.googlecode.com/hg/Resources/Configuration.json"); - c.Apply(v); - ASSERT_TRUE(v.isMember("StorageDirectory")); - //ASSERT_EQ(GetLastStatusText()); - - v = Json::nullValue; - - HttpClient cc(c); - cc.SetUrl("https://orthanc.googlecode.com/hg/Resources/Configuration.json"); - cc.Apply(v); - ASSERT_TRUE(v.isMember("LuaScripts")); -#endif -} - -TEST(RestApi, ChunkedBuffer) -{ - ChunkedBuffer b; - ASSERT_EQ(0, b.GetNumBytes()); - - b.AddChunk("hello", 5); - ASSERT_EQ(5, b.GetNumBytes()); - - b.AddChunk("world", 5); - ASSERT_EQ(10, b.GetNumBytes()); - - std::string s; - b.Flatten(s); - ASSERT_EQ("helloworld", s); -} - -TEST(RestApi, ParseCookies) -{ - HttpHandler::Arguments headers; - HttpHandler::Arguments cookies; - - headers["cookie"] = "a=b;c=d;;;e=f;;g=h;"; - HttpHandler::ParseCookies(cookies, headers); - ASSERT_EQ(4u, cookies.size()); - ASSERT_EQ("b", cookies["a"]); - ASSERT_EQ("d", cookies["c"]); - ASSERT_EQ("f", cookies["e"]); - ASSERT_EQ("h", cookies["g"]); - - headers["cookie"] = " name = value ; name2=value2"; - HttpHandler::ParseCookies(cookies, headers); - ASSERT_EQ(2u, cookies.size()); - ASSERT_EQ("value", cookies["name"]); - ASSERT_EQ("value2", cookies["name2"]); - - headers["cookie"] = " ;;; "; - HttpHandler::ParseCookies(cookies, headers); - ASSERT_EQ(0u, cookies.size()); - - headers["cookie"] = " ; n=v ;; "; - HttpHandler::ParseCookies(cookies, headers); - ASSERT_EQ(1u, cookies.size()); - ASSERT_EQ("v", cookies["n"]); -} - -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]); - } -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/UnitTestsSources/RestApiTests.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,283 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "PrecompiledHeadersUnitTests.h" +#include "gtest/gtest.h" + +#include <ctype.h> +#include <glog/logging.h> + +#include "../Core/ChunkedBuffer.h" +#include "../Core/HttpClient.h" +#include "../Core/RestApi/RestApi.h" +#include "../Core/Uuid.h" +#include "../Core/OrthancException.h" +#include "../Core/Compression/ZlibCompressor.h" +#include "../Core/RestApi/RestApiHierarchy.h" + +using namespace Orthanc; + +#if !defined(UNIT_TESTS_WITH_HTTP_CONNEXIONS) +#error "Please set UNIT_TESTS_WITH_HTTP_CONNEXIONS" +#endif + +TEST(HttpClient, Basic) +{ + HttpClient c; + ASSERT_FALSE(c.IsVerbose()); + c.SetVerbose(true); + ASSERT_TRUE(c.IsVerbose()); + c.SetVerbose(false); + ASSERT_FALSE(c.IsVerbose()); + +#if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1 + Json::Value v; + c.SetUrl("http://orthanc.googlecode.com/hg/Resources/Configuration.json"); + c.Apply(v); + ASSERT_TRUE(v.isMember("StorageDirectory")); + //ASSERT_EQ(GetLastStatusText()); + + v = Json::nullValue; + + HttpClient cc(c); + cc.SetUrl("https://orthanc.googlecode.com/hg/Resources/Configuration.json"); + cc.Apply(v); + ASSERT_TRUE(v.isMember("LuaScripts")); +#endif +} + +TEST(RestApi, ChunkedBuffer) +{ + ChunkedBuffer b; + ASSERT_EQ(0, b.GetNumBytes()); + + b.AddChunk("hello", 5); + ASSERT_EQ(5, b.GetNumBytes()); + + b.AddChunk("world", 5); + ASSERT_EQ(10, b.GetNumBytes()); + + std::string s; + b.Flatten(s); + ASSERT_EQ("helloworld", s); +} + +TEST(RestApi, ParseCookies) +{ + HttpHandler::Arguments headers; + HttpHandler::Arguments cookies; + + headers["cookie"] = "a=b;c=d;;;e=f;;g=h;"; + HttpHandler::ParseCookies(cookies, headers); + ASSERT_EQ(4u, cookies.size()); + ASSERT_EQ("b", cookies["a"]); + ASSERT_EQ("d", cookies["c"]); + ASSERT_EQ("f", cookies["e"]); + ASSERT_EQ("h", cookies["g"]); + + headers["cookie"] = " name = value ; name2=value2"; + HttpHandler::ParseCookies(cookies, headers); + ASSERT_EQ(2u, cookies.size()); + ASSERT_EQ("value", cookies["name"]); + ASSERT_EQ("value2", cookies["name2"]); + + headers["cookie"] = " ;;; "; + HttpHandler::ParseCookies(cookies, headers); + ASSERT_EQ(0u, cookies.size()); + + headers["cookie"] = " ; n=v ;; "; + HttpHandler::ParseCookies(cookies, headers); + ASSERT_EQ(1u, cookies.size()); + ASSERT_EQ("v", cookies["n"]); +} + +TEST(RestApi, RestApiPath) +{ + HttpHandler::Arguments 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")); + + ASSERT_EQ(3u, uri.GetLevelCount()); + ASSERT_TRUE(uri.IsUniversalTrailing()); + + ASSERT_EQ("coucou", uri.GetLevelName(0)); + ASSERT_THROW(uri.GetWildcardName(0), OrthancException); + + ASSERT_EQ("abc", uri.GetWildcardName(1)); + ASSERT_THROW(uri.GetLevelName(1), OrthancException); + + ASSERT_EQ("d", uri.GetLevelName(2)); + ASSERT_THROW(uri.GetWildcardName(2), OrthancException); + } + + { + 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"]); + + ASSERT_EQ(3u, uri.GetLevelCount()); + ASSERT_FALSE(uri.IsUniversalTrailing()); + + ASSERT_EQ("coucou", uri.GetLevelName(0)); + ASSERT_THROW(uri.GetWildcardName(0), OrthancException); + + ASSERT_EQ("abc", uri.GetWildcardName(1)); + ASSERT_THROW(uri.GetLevelName(1), OrthancException); + + ASSERT_EQ("d", uri.GetLevelName(2)); + ASSERT_THROW(uri.GetWildcardName(2), OrthancException); + } + + { + 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]); + + ASSERT_EQ(0u, uri.GetLevelCount()); + ASSERT_TRUE(uri.IsUniversalTrailing()); + } +} + + + + + + +static int testValue; + +template <int value> +static void SetValue(RestApiGetCall& get) +{ + testValue = value; +} + + +static bool GetDirectory(Json::Value& target, + RestApiHierarchy& hierarchy, + const std::string& uri) +{ + UriComponents p; + Toolbox::SplitUriComponents(p, uri); + return hierarchy.GetDirectory(target, p); +} + + + +namespace +{ + class MyVisitor : public RestApiHierarchy::IVisitor + { + public: + virtual bool Visit(const RestApiHierarchy::Resource& resource, + const UriComponents& uri, + const HttpHandler::Arguments& components, + const UriComponents& trailing) + { + return resource.Handle(*reinterpret_cast<RestApiGetCall*>(NULL)); + } + }; +} + + +static bool HandleGet(RestApiHierarchy& hierarchy, + const std::string& uri) +{ + UriComponents p; + Toolbox::SplitUriComponents(p, uri); + MyVisitor visitor; + return hierarchy.LookupResource(p, visitor); +} + + +TEST(RestApi, RestApiHierarchy) +{ + RestApiHierarchy root; + root.Register("/hello/world/test", SetValue<1>); + root.Register("/hello/world/test2", SetValue<2>); + root.Register("/hello/{world}/test3/test4", SetValue<3>); + root.Register("/hello2/*", SetValue<4>); + + Json::Value m; + root.CreateSiteMap(m); + std::cout << m; + + Json::Value d; + ASSERT_FALSE(GetDirectory(d, root, "/hello")); + + ASSERT_TRUE(GetDirectory(d, root, "/hello/a")); + ASSERT_EQ(1u, d.size()); + ASSERT_EQ("test3", d[0].asString()); + + ASSERT_TRUE(GetDirectory(d, root, "/hello/world")); + ASSERT_EQ(2u, d.size()); + + ASSERT_TRUE(GetDirectory(d, root, "/hello/a/test3")); + ASSERT_EQ(1u, d.size()); + ASSERT_EQ("test4", d[0].asString()); + + ASSERT_TRUE(GetDirectory(d, root, "/hello/world/test")); + ASSERT_TRUE(GetDirectory(d, root, "/hello/world/test2")); + ASSERT_FALSE(GetDirectory(d, root, "/hello2")); + + testValue = 0; + ASSERT_TRUE(HandleGet(root, "/hello/world/test")); + ASSERT_EQ(testValue, 1); + ASSERT_TRUE(HandleGet(root, "/hello/world/test2")); + ASSERT_EQ(testValue, 2); + ASSERT_TRUE(HandleGet(root, "/hello/b/test3/test4")); + ASSERT_EQ(testValue, 3); + ASSERT_FALSE(HandleGet(root, "/hello/b/test3/test")); + ASSERT_EQ(testValue, 3); + ASSERT_TRUE(HandleGet(root, "/hello2/a/b")); + ASSERT_EQ(testValue, 4); +}
--- a/UnitTestsSources/SQLite.cpp Wed Jun 25 15:34:40 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,334 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - **/ - - -#include "PrecompiledHeadersUnitTests.h" -#include "gtest/gtest.h" - -#include "../Core/Toolbox.h" -#include "../Core/SQLite/Connection.h" -#include "../Core/SQLite/Statement.h" -#include "../Core/SQLite/Transaction.h" - -#include <sqlite3.h> - -using namespace Orthanc; - - -TEST(SQLite, Configuration) -{ - ASSERT_EQ(1, sqlite3_threadsafe()); -} - - -TEST(SQLite, Connection) -{ - Toolbox::RemoveFile("UnitTestsResults/coucou"); - SQLite::Connection c; - c.Open("UnitTestsResults/coucou"); - c.Execute("CREATE TABLE c(k INTEGER PRIMARY KEY AUTOINCREMENT, v INTEGER)"); - c.Execute("INSERT INTO c VALUES(NULL, 42);"); -} - - -TEST(SQLite, StatementReferenceBasic) -{ - sqlite3* db; - sqlite3_open(":memory:", &db); - - { - SQLite::StatementReference r(db, "SELECT * FROM sqlite_master"); - ASSERT_EQ(0u, r.GetReferenceCount()); - - { - SQLite::StatementReference r1(r); - ASSERT_EQ(1u, r.GetReferenceCount()); - ASSERT_EQ(0u, r1.GetReferenceCount()); - - { - SQLite::StatementReference r2(r); - ASSERT_EQ(2u, r.GetReferenceCount()); - ASSERT_EQ(0u, r1.GetReferenceCount()); - ASSERT_EQ(0u, r2.GetReferenceCount()); - - SQLite::StatementReference r3(r2); - ASSERT_EQ(3u, r.GetReferenceCount()); - ASSERT_EQ(0u, r1.GetReferenceCount()); - ASSERT_EQ(0u, r2.GetReferenceCount()); - ASSERT_EQ(0u, r3.GetReferenceCount()); - } - - ASSERT_EQ(1u, r.GetReferenceCount()); - ASSERT_EQ(0u, r1.GetReferenceCount()); - - { - SQLite::StatementReference r2(r); - ASSERT_EQ(2u, r.GetReferenceCount()); - ASSERT_EQ(0u, r1.GetReferenceCount()); - ASSERT_EQ(0u, r2.GetReferenceCount()); - } - - ASSERT_EQ(1u, r.GetReferenceCount()); - ASSERT_EQ(0u, r1.GetReferenceCount()); - } - - ASSERT_EQ(0u, r.GetReferenceCount()); - } - - sqlite3_close(db); -} - -TEST(SQLite, StatementBasic) -{ - SQLite::Connection c; - c.OpenInMemory(); - - SQLite::Statement s(c, "SELECT * from sqlite_master"); - s.Run(); - - for (unsigned int i = 0; i < 5; i++) - { - SQLite::Statement cs(c, SQLITE_FROM_HERE, "SELECT * from sqlite_master"); - cs.Step(); - } -} - - -namespace -{ - static bool destroyed; - - class MyFunc : public SQLite::IScalarFunction - { - public: - MyFunc() - { - destroyed = false; - } - - virtual ~MyFunc() - { - destroyed = true; - } - - virtual const char* GetName() const - { - return "MYFUNC"; - } - - virtual unsigned int GetCardinality() const - { - return 2; - } - - virtual void Compute(SQLite::FunctionContext& context) - { - context.SetIntResult(1000 + context.GetIntValue(0) * context.GetIntValue(1)); - } - }; - - class MyDelete : public SQLite::IScalarFunction - { - public: - std::set<int> deleted_; - - virtual const char* GetName() const - { - return "MYDELETE"; - } - - virtual unsigned int GetCardinality() const - { - return 1; - } - - virtual void Compute(SQLite::FunctionContext& context) - { - deleted_.insert(context.GetIntValue(0)); - context.SetNullResult(); - } - }; -} - -TEST(SQLite, ScalarFunction) -{ - { - SQLite::Connection c; - c.OpenInMemory(); - c.Register(new MyFunc()); - c.Execute("CREATE TABLE t(id INTEGER PRIMARY KEY, v1 INTEGER, v2 INTEGER);"); - c.Execute("INSERT INTO t VALUES(NULL, 2, 3);"); - c.Execute("INSERT INTO t VALUES(NULL, 4, 4);"); - c.Execute("INSERT INTO t VALUES(NULL, 6, 5);"); - SQLite::Statement t(c, "SELECT MYFUNC(v1, v2), v1, v2 FROM t"); - int i = 0; - while (t.Step()) - { - ASSERT_EQ(t.ColumnInt(0), 1000 + t.ColumnInt(1) * t.ColumnInt(2)); - i++; - } - ASSERT_EQ(3, i); - ASSERT_FALSE(destroyed); - } - ASSERT_TRUE(destroyed); -} - -TEST(SQLite, CascadedDeleteCallback) -{ - SQLite::Connection c; - c.OpenInMemory(); - MyDelete *func = new MyDelete(); - c.Register(func); - c.Execute("CREATE TABLE parent(id INTEGER PRIMARY KEY, dummy INTEGER);"); - c.Execute("CREATE TABLE child(" - " id INTEGER PRIMARY KEY, " - " parent INTEGER REFERENCES parent(id) ON DELETE CASCADE, " - " value INTEGER);"); - c.Execute("CREATE TRIGGER childRemoved " - "AFTER DELETE ON child " - "FOR EACH ROW BEGIN " - " SELECT MYDELETE(old.value); " - "END;"); - - c.Execute("INSERT INTO parent VALUES(42, 100);"); - c.Execute("INSERT INTO parent VALUES(43, 101);"); - - c.Execute("INSERT INTO child VALUES(NULL, 42, 4200);"); - c.Execute("INSERT INTO child VALUES(NULL, 42, 4201);"); - - c.Execute("INSERT INTO child VALUES(NULL, 43, 4300);"); - c.Execute("INSERT INTO child VALUES(NULL, 43, 4301);"); - - // The following command deletes "parent(43, 101)", then in turns - // "child(NULL, 43, 4300/4301)", then calls the MyDelete on 4300 and - // 4301 - c.Execute("DELETE FROM parent WHERE dummy=101"); - - ASSERT_EQ(2u, func->deleted_.size()); - 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; - } -} - - -TEST(SQLite, Types) -{ - SQLite::Connection c; - c.OpenInMemory(); - c.Execute("CREATE TABLE a(id INTEGER PRIMARY KEY, value)"); - - { - SQLite::Statement s(c, std::string("SELECT * FROM a")); - ASSERT_EQ(2, s.ColumnCount()); - ASSERT_FALSE(s.Step()); - } - - { - SQLite::Statement s(c, SQLITE_FROM_HERE, std::string("SELECT * FROM a")); - ASSERT_FALSE(s.Step()); - ASSERT_EQ("SELECT * FROM a", s.GetOriginalSQLStatement()); - } - - { - SQLite::Statement s(c, SQLITE_FROM_HERE, "INSERT INTO a VALUES(NULL, ?);"); - s.BindNull(0); ASSERT_TRUE(s.Run()); s.Reset(); - s.BindBool(0, true); ASSERT_TRUE(s.Run()); s.Reset(); - s.BindInt(0, 42); ASSERT_TRUE(s.Run()); s.Reset(); - s.BindInt64(0, 42ll); ASSERT_TRUE(s.Run()); s.Reset(); - s.BindDouble(0, 42.5); ASSERT_TRUE(s.Run()); s.Reset(); - s.BindCString(0, "Hello"); ASSERT_TRUE(s.Run()); s.Reset(); - s.BindBlob(0, "Hello", 5); ASSERT_TRUE(s.Run()); s.Reset(); - } - - { - SQLite::Statement s(c, SQLITE_FROM_HERE, std::string("SELECT * FROM a")); - ASSERT_TRUE(s.Step()); - ASSERT_EQ(SQLite::COLUMN_TYPE_NULL, s.GetColumnType(1)); - ASSERT_TRUE(s.ColumnIsNull(1)); - ASSERT_TRUE(s.Step()); - ASSERT_EQ(SQLite::COLUMN_TYPE_INTEGER, s.GetColumnType(1)); - ASSERT_TRUE(s.ColumnBool(1)); - ASSERT_TRUE(s.Step()); - ASSERT_EQ(SQLite::COLUMN_TYPE_INTEGER, s.GetColumnType(1)); - ASSERT_EQ(42, s.ColumnInt(1)); - ASSERT_TRUE(s.Step()); - ASSERT_EQ(SQLite::COLUMN_TYPE_INTEGER, s.GetColumnType(1)); - ASSERT_EQ(42ll, s.ColumnInt64(1)); - ASSERT_TRUE(s.Step()); - ASSERT_EQ(SQLite::COLUMN_TYPE_FLOAT, s.GetColumnType(1)); - ASSERT_DOUBLE_EQ(42.5, s.ColumnDouble(1)); - ASSERT_TRUE(s.Step()); - ASSERT_EQ(SQLite::COLUMN_TYPE_TEXT, s.GetColumnType(1)); - ASSERT_EQ("Hello", s.ColumnString(1)); - ASSERT_TRUE(s.Step()); - ASSERT_EQ(SQLite::COLUMN_TYPE_BLOB, s.GetColumnType(1)); - ASSERT_EQ(5, s.ColumnByteLength(1)); - ASSERT_TRUE(!memcmp("Hello", s.ColumnBlob(1), 5)); - - std::string t; - ASSERT_TRUE(s.ColumnBlobAsString(1, &t)); - ASSERT_EQ("Hello", t); - - ASSERT_FALSE(s.Step()); - } -}
--- a/UnitTestsSources/SQLiteChromium.cpp Wed Jun 25 15:34:40 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,377 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - **/ - - -#include "PrecompiledHeadersUnitTests.h" -#include "gtest/gtest.h" - -#include "../Core/Toolbox.h" -#include "../Core/SQLite/Connection.h" -#include "../Core/SQLite/Statement.h" -#include "../Core/SQLite/Transaction.h" - -#include <sqlite3.h> - - -using namespace Orthanc; -using namespace Orthanc::SQLite; - - -/******************************************************************** - ** Tests from - ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/connection_unittest.cc - ********************************************************************/ - -class SQLConnectionTest : public testing::Test -{ -public: - SQLConnectionTest() - { - } - - virtual ~SQLConnectionTest() - { - } - - virtual void SetUp() - { - db_.OpenInMemory(); - } - - virtual void TearDown() - { - db_.Close(); - } - - Connection& db() - { - return db_; - } - -private: - Connection db_; -}; - - - -TEST_F(SQLConnectionTest, Execute) -{ - // Valid statement should return true. - ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); - EXPECT_EQ(SQLITE_OK, db().GetErrorCode()); - - // Invalid statement should fail. - ASSERT_EQ(SQLITE_ERROR, - db().ExecuteAndReturnErrorCode("CREATE TAB foo (a, b")); - EXPECT_EQ(SQLITE_ERROR, db().GetErrorCode()); -} - -TEST_F(SQLConnectionTest, ExecuteWithErrorCode) { - ASSERT_EQ(SQLITE_OK, - db().ExecuteAndReturnErrorCode("CREATE TABLE foo (a, b)")); - ASSERT_EQ(SQLITE_ERROR, - db().ExecuteAndReturnErrorCode("CREATE TABLE TABLE")); - ASSERT_EQ(SQLITE_ERROR, - db().ExecuteAndReturnErrorCode( - "INSERT INTO foo(a, b) VALUES (1, 2, 3, 4)")); -} - -TEST_F(SQLConnectionTest, CachedStatement) { - 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. - { - Statement s(db(), id1, "SELECT a FROM foo"); - ASSERT_TRUE(s.Step()); - EXPECT_EQ(12, s.ColumnInt(0)); - } - - // The statement should be cached still. - EXPECT_TRUE(db().HasCachedStatement(id1)); - - { - // Get the same statement using different SQL. This should ignore our - // SQL and use the cached one (so it will be valid). - Statement s(db(), id1, "something invalid("); - ASSERT_TRUE(s.Step()); - EXPECT_EQ(12, s.ColumnInt(0)); - } - - // Make sure other statements aren't marked as cached. - EXPECT_FALSE(db().HasCachedStatement(SQLITE_FROM_HERE)); -} - -TEST_F(SQLConnectionTest, IsSQLValidTest) { - ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); - ASSERT_TRUE(db().IsSQLValid("SELECT a FROM foo")); - ASSERT_FALSE(db().IsSQLValid("SELECT no_exist FROM foo")); -} - - - -TEST_F(SQLConnectionTest, DoesStuffExist) { - // Test DoesTableExist. - EXPECT_FALSE(db().DoesTableExist("foo")); - ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); - EXPECT_TRUE(db().DoesTableExist("foo")); - - // Should be case sensitive. - EXPECT_FALSE(db().DoesTableExist("FOO")); - - // Test DoesColumnExist. - EXPECT_FALSE(db().DoesColumnExist("foo", "bar")); - EXPECT_TRUE(db().DoesColumnExist("foo", "a")); - - // Testing for a column on a nonexistent table. - EXPECT_FALSE(db().DoesColumnExist("bar", "b")); -} - -TEST_F(SQLConnectionTest, GetLastInsertRowId) { - ASSERT_TRUE(db().Execute("CREATE TABLE foo (id INTEGER PRIMARY KEY, value)")); - - ASSERT_TRUE(db().Execute("INSERT INTO foo (value) VALUES (12)")); - - // Last insert row ID should be valid. - int64_t row = db().GetLastInsertRowId(); - EXPECT_LT(0, row); - - // It should be the primary key of the row we just inserted. - Statement s(db(), "SELECT value FROM foo WHERE id=?"); - s.BindInt64(0, row); - ASSERT_TRUE(s.Step()); - EXPECT_EQ(12, s.ColumnInt(0)); -} - -TEST_F(SQLConnectionTest, Rollback) { - ASSERT_TRUE(db().BeginTransaction()); - ASSERT_TRUE(db().BeginTransaction()); - EXPECT_EQ(2, db().GetTransactionNesting()); - db().RollbackTransaction(); - EXPECT_FALSE(db().CommitTransaction()); - EXPECT_TRUE(db().BeginTransaction()); -} - - - - -/******************************************************************** - ** Tests from - ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/statement_unittest.cc - ********************************************************************/ - -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)")); - - 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()); - - // 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()); - } - - 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)")); - - 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(true); - ASSERT_FALSE(s.Step()); - } - } -} - - - - - - -/******************************************************************** - ** Tests from - ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/transaction_unittest.cc - ********************************************************************/ - -class SQLTransactionTest : public SQLConnectionTest -{ -public: - virtual void SetUp() - { - SQLConnectionTest::SetUp(); - ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); - } - - // Returns the number of rows in table "foo". - int CountFoo() - { - Statement count(db(), "SELECT count(*) FROM foo"); - count.Step(); - return count.ColumnInt(0); - } -}; - - -TEST_F(SQLTransactionTest, Commit) { - { - Transaction t(db()); - EXPECT_FALSE(t.IsOpen()); - t.Begin(); - EXPECT_TRUE(t.IsOpen()); - - EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)")); - - t.Commit(); - EXPECT_FALSE(t.IsOpen()); - } - - EXPECT_EQ(1, CountFoo()); -} - -TEST_F(SQLTransactionTest, Rollback) { - // Test some basic initialization, and that rollback runs when you exit the - // scope. - { - Transaction t(db()); - EXPECT_FALSE(t.IsOpen()); - t.Begin(); - EXPECT_TRUE(t.IsOpen()); - - EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)")); - } - - // Nothing should have been committed since it was implicitly rolled back. - EXPECT_EQ(0, CountFoo()); - - // Test explicit rollback. - Transaction t2(db()); - EXPECT_FALSE(t2.IsOpen()); - t2.Begin(); - - EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)")); - t2.Rollback(); - EXPECT_FALSE(t2.IsOpen()); - - // Nothing should have been committed since it was explicitly rolled back. - EXPECT_EQ(0, CountFoo()); -} - -// Rolling back any part of a transaction should roll back all of them. -TEST_F(SQLTransactionTest, NestedRollback) { - EXPECT_EQ(0, db().GetTransactionNesting()); - - // Outermost transaction. - { - Transaction outer(db()); - outer.Begin(); - EXPECT_EQ(1, db().GetTransactionNesting()); - - // The first inner one gets committed. - { - Transaction inner1(db()); - inner1.Begin(); - EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)")); - EXPECT_EQ(2, db().GetTransactionNesting()); - - inner1.Commit(); - EXPECT_EQ(1, db().GetTransactionNesting()); - } - - // One row should have gotten inserted. - EXPECT_EQ(1, CountFoo()); - - // The second inner one gets rolled back. - { - Transaction inner2(db()); - inner2.Begin(); - EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)")); - EXPECT_EQ(2, db().GetTransactionNesting()); - - inner2.Rollback(); - EXPECT_EQ(1, db().GetTransactionNesting()); - } - - // A third inner one will fail in Begin since one has already been rolled - // back. - EXPECT_EQ(1, db().GetTransactionNesting()); - { - Transaction inner3(db()); - EXPECT_THROW(inner3.Begin(), OrthancException); - EXPECT_EQ(1, db().GetTransactionNesting()); - } - } - EXPECT_EQ(0, db().GetTransactionNesting()); - EXPECT_EQ(0, CountFoo()); -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/UnitTestsSources/SQLiteChromiumTests.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,377 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "PrecompiledHeadersUnitTests.h" +#include "gtest/gtest.h" + +#include "../Core/Toolbox.h" +#include "../Core/SQLite/Connection.h" +#include "../Core/SQLite/Statement.h" +#include "../Core/SQLite/Transaction.h" + +#include <sqlite3.h> + + +using namespace Orthanc; +using namespace Orthanc::SQLite; + + +/******************************************************************** + ** Tests from + ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/connection_unittest.cc + ********************************************************************/ + +class SQLConnectionTest : public testing::Test +{ +public: + SQLConnectionTest() + { + } + + virtual ~SQLConnectionTest() + { + } + + virtual void SetUp() + { + db_.OpenInMemory(); + } + + virtual void TearDown() + { + db_.Close(); + } + + Connection& db() + { + return db_; + } + +private: + Connection db_; +}; + + + +TEST_F(SQLConnectionTest, Execute) +{ + // Valid statement should return true. + ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); + EXPECT_EQ(SQLITE_OK, db().GetErrorCode()); + + // Invalid statement should fail. + ASSERT_EQ(SQLITE_ERROR, + db().ExecuteAndReturnErrorCode("CREATE TAB foo (a, b")); + EXPECT_EQ(SQLITE_ERROR, db().GetErrorCode()); +} + +TEST_F(SQLConnectionTest, ExecuteWithErrorCode) { + ASSERT_EQ(SQLITE_OK, + db().ExecuteAndReturnErrorCode("CREATE TABLE foo (a, b)")); + ASSERT_EQ(SQLITE_ERROR, + db().ExecuteAndReturnErrorCode("CREATE TABLE TABLE")); + ASSERT_EQ(SQLITE_ERROR, + db().ExecuteAndReturnErrorCode( + "INSERT INTO foo(a, b) VALUES (1, 2, 3, 4)")); +} + +TEST_F(SQLConnectionTest, CachedStatement) { + 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. + { + Statement s(db(), id1, "SELECT a FROM foo"); + ASSERT_TRUE(s.Step()); + EXPECT_EQ(12, s.ColumnInt(0)); + } + + // The statement should be cached still. + EXPECT_TRUE(db().HasCachedStatement(id1)); + + { + // Get the same statement using different SQL. This should ignore our + // SQL and use the cached one (so it will be valid). + Statement s(db(), id1, "something invalid("); + ASSERT_TRUE(s.Step()); + EXPECT_EQ(12, s.ColumnInt(0)); + } + + // Make sure other statements aren't marked as cached. + EXPECT_FALSE(db().HasCachedStatement(SQLITE_FROM_HERE)); +} + +TEST_F(SQLConnectionTest, IsSQLValidTest) { + ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); + ASSERT_TRUE(db().IsSQLValid("SELECT a FROM foo")); + ASSERT_FALSE(db().IsSQLValid("SELECT no_exist FROM foo")); +} + + + +TEST_F(SQLConnectionTest, DoesStuffExist) { + // Test DoesTableExist. + EXPECT_FALSE(db().DoesTableExist("foo")); + ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); + EXPECT_TRUE(db().DoesTableExist("foo")); + + // Should be case sensitive. + EXPECT_FALSE(db().DoesTableExist("FOO")); + + // Test DoesColumnExist. + EXPECT_FALSE(db().DoesColumnExist("foo", "bar")); + EXPECT_TRUE(db().DoesColumnExist("foo", "a")); + + // Testing for a column on a nonexistent table. + EXPECT_FALSE(db().DoesColumnExist("bar", "b")); +} + +TEST_F(SQLConnectionTest, GetLastInsertRowId) { + ASSERT_TRUE(db().Execute("CREATE TABLE foo (id INTEGER PRIMARY KEY, value)")); + + ASSERT_TRUE(db().Execute("INSERT INTO foo (value) VALUES (12)")); + + // Last insert row ID should be valid. + int64_t row = db().GetLastInsertRowId(); + EXPECT_LT(0, row); + + // It should be the primary key of the row we just inserted. + Statement s(db(), "SELECT value FROM foo WHERE id=?"); + s.BindInt64(0, row); + ASSERT_TRUE(s.Step()); + EXPECT_EQ(12, s.ColumnInt(0)); +} + +TEST_F(SQLConnectionTest, Rollback) { + ASSERT_TRUE(db().BeginTransaction()); + ASSERT_TRUE(db().BeginTransaction()); + EXPECT_EQ(2, db().GetTransactionNesting()); + db().RollbackTransaction(); + EXPECT_FALSE(db().CommitTransaction()); + EXPECT_TRUE(db().BeginTransaction()); +} + + + + +/******************************************************************** + ** Tests from + ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/statement_unittest.cc + ********************************************************************/ + +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)")); + + 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()); + + // 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()); + } + + 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)")); + + 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(true); + ASSERT_FALSE(s.Step()); + } + } +} + + + + + + +/******************************************************************** + ** Tests from + ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/transaction_unittest.cc + ********************************************************************/ + +class SQLTransactionTest : public SQLConnectionTest +{ +public: + virtual void SetUp() + { + SQLConnectionTest::SetUp(); + ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); + } + + // Returns the number of rows in table "foo". + int CountFoo() + { + Statement count(db(), "SELECT count(*) FROM foo"); + count.Step(); + return count.ColumnInt(0); + } +}; + + +TEST_F(SQLTransactionTest, Commit) { + { + Transaction t(db()); + EXPECT_FALSE(t.IsOpen()); + t.Begin(); + EXPECT_TRUE(t.IsOpen()); + + EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)")); + + t.Commit(); + EXPECT_FALSE(t.IsOpen()); + } + + EXPECT_EQ(1, CountFoo()); +} + +TEST_F(SQLTransactionTest, Rollback) { + // Test some basic initialization, and that rollback runs when you exit the + // scope. + { + Transaction t(db()); + EXPECT_FALSE(t.IsOpen()); + t.Begin(); + EXPECT_TRUE(t.IsOpen()); + + EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)")); + } + + // Nothing should have been committed since it was implicitly rolled back. + EXPECT_EQ(0, CountFoo()); + + // Test explicit rollback. + Transaction t2(db()); + EXPECT_FALSE(t2.IsOpen()); + t2.Begin(); + + EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)")); + t2.Rollback(); + EXPECT_FALSE(t2.IsOpen()); + + // Nothing should have been committed since it was explicitly rolled back. + EXPECT_EQ(0, CountFoo()); +} + +// Rolling back any part of a transaction should roll back all of them. +TEST_F(SQLTransactionTest, NestedRollback) { + EXPECT_EQ(0, db().GetTransactionNesting()); + + // Outermost transaction. + { + Transaction outer(db()); + outer.Begin(); + EXPECT_EQ(1, db().GetTransactionNesting()); + + // The first inner one gets committed. + { + Transaction inner1(db()); + inner1.Begin(); + EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)")); + EXPECT_EQ(2, db().GetTransactionNesting()); + + inner1.Commit(); + EXPECT_EQ(1, db().GetTransactionNesting()); + } + + // One row should have gotten inserted. + EXPECT_EQ(1, CountFoo()); + + // The second inner one gets rolled back. + { + Transaction inner2(db()); + inner2.Begin(); + EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)")); + EXPECT_EQ(2, db().GetTransactionNesting()); + + inner2.Rollback(); + EXPECT_EQ(1, db().GetTransactionNesting()); + } + + // A third inner one will fail in Begin since one has already been rolled + // back. + EXPECT_EQ(1, db().GetTransactionNesting()); + { + Transaction inner3(db()); + EXPECT_THROW(inner3.Begin(), OrthancException); + EXPECT_EQ(1, db().GetTransactionNesting()); + } + } + EXPECT_EQ(0, db().GetTransactionNesting()); + EXPECT_EQ(0, CountFoo()); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/UnitTestsSources/SQLiteTests.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,334 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "PrecompiledHeadersUnitTests.h" +#include "gtest/gtest.h" + +#include "../Core/Toolbox.h" +#include "../Core/SQLite/Connection.h" +#include "../Core/SQLite/Statement.h" +#include "../Core/SQLite/Transaction.h" + +#include <sqlite3.h> + +using namespace Orthanc; + + +TEST(SQLite, Configuration) +{ + ASSERT_EQ(1, sqlite3_threadsafe()); +} + + +TEST(SQLite, Connection) +{ + Toolbox::RemoveFile("UnitTestsResults/coucou"); + SQLite::Connection c; + c.Open("UnitTestsResults/coucou"); + c.Execute("CREATE TABLE c(k INTEGER PRIMARY KEY AUTOINCREMENT, v INTEGER)"); + c.Execute("INSERT INTO c VALUES(NULL, 42);"); +} + + +TEST(SQLite, StatementReferenceBasic) +{ + sqlite3* db; + sqlite3_open(":memory:", &db); + + { + SQLite::StatementReference r(db, "SELECT * FROM sqlite_master"); + ASSERT_EQ(0u, r.GetReferenceCount()); + + { + SQLite::StatementReference r1(r); + ASSERT_EQ(1u, r.GetReferenceCount()); + ASSERT_EQ(0u, r1.GetReferenceCount()); + + { + SQLite::StatementReference r2(r); + ASSERT_EQ(2u, r.GetReferenceCount()); + ASSERT_EQ(0u, r1.GetReferenceCount()); + ASSERT_EQ(0u, r2.GetReferenceCount()); + + SQLite::StatementReference r3(r2); + ASSERT_EQ(3u, r.GetReferenceCount()); + ASSERT_EQ(0u, r1.GetReferenceCount()); + ASSERT_EQ(0u, r2.GetReferenceCount()); + ASSERT_EQ(0u, r3.GetReferenceCount()); + } + + ASSERT_EQ(1u, r.GetReferenceCount()); + ASSERT_EQ(0u, r1.GetReferenceCount()); + + { + SQLite::StatementReference r2(r); + ASSERT_EQ(2u, r.GetReferenceCount()); + ASSERT_EQ(0u, r1.GetReferenceCount()); + ASSERT_EQ(0u, r2.GetReferenceCount()); + } + + ASSERT_EQ(1u, r.GetReferenceCount()); + ASSERT_EQ(0u, r1.GetReferenceCount()); + } + + ASSERT_EQ(0u, r.GetReferenceCount()); + } + + sqlite3_close(db); +} + +TEST(SQLite, StatementBasic) +{ + SQLite::Connection c; + c.OpenInMemory(); + + SQLite::Statement s(c, "SELECT * from sqlite_master"); + s.Run(); + + for (unsigned int i = 0; i < 5; i++) + { + SQLite::Statement cs(c, SQLITE_FROM_HERE, "SELECT * from sqlite_master"); + cs.Step(); + } +} + + +namespace +{ + static bool destroyed; + + class MyFunc : public SQLite::IScalarFunction + { + public: + MyFunc() + { + destroyed = false; + } + + virtual ~MyFunc() + { + destroyed = true; + } + + virtual const char* GetName() const + { + return "MYFUNC"; + } + + virtual unsigned int GetCardinality() const + { + return 2; + } + + virtual void Compute(SQLite::FunctionContext& context) + { + context.SetIntResult(1000 + context.GetIntValue(0) * context.GetIntValue(1)); + } + }; + + class MyDelete : public SQLite::IScalarFunction + { + public: + std::set<int> deleted_; + + virtual const char* GetName() const + { + return "MYDELETE"; + } + + virtual unsigned int GetCardinality() const + { + return 1; + } + + virtual void Compute(SQLite::FunctionContext& context) + { + deleted_.insert(context.GetIntValue(0)); + context.SetNullResult(); + } + }; +} + +TEST(SQLite, ScalarFunction) +{ + { + SQLite::Connection c; + c.OpenInMemory(); + c.Register(new MyFunc()); + c.Execute("CREATE TABLE t(id INTEGER PRIMARY KEY, v1 INTEGER, v2 INTEGER);"); + c.Execute("INSERT INTO t VALUES(NULL, 2, 3);"); + c.Execute("INSERT INTO t VALUES(NULL, 4, 4);"); + c.Execute("INSERT INTO t VALUES(NULL, 6, 5);"); + SQLite::Statement t(c, "SELECT MYFUNC(v1, v2), v1, v2 FROM t"); + int i = 0; + while (t.Step()) + { + ASSERT_EQ(t.ColumnInt(0), 1000 + t.ColumnInt(1) * t.ColumnInt(2)); + i++; + } + ASSERT_EQ(3, i); + ASSERT_FALSE(destroyed); + } + ASSERT_TRUE(destroyed); +} + +TEST(SQLite, CascadedDeleteCallback) +{ + SQLite::Connection c; + c.OpenInMemory(); + MyDelete *func = new MyDelete(); + c.Register(func); + c.Execute("CREATE TABLE parent(id INTEGER PRIMARY KEY, dummy INTEGER);"); + c.Execute("CREATE TABLE child(" + " id INTEGER PRIMARY KEY, " + " parent INTEGER REFERENCES parent(id) ON DELETE CASCADE, " + " value INTEGER);"); + c.Execute("CREATE TRIGGER childRemoved " + "AFTER DELETE ON child " + "FOR EACH ROW BEGIN " + " SELECT MYDELETE(old.value); " + "END;"); + + c.Execute("INSERT INTO parent VALUES(42, 100);"); + c.Execute("INSERT INTO parent VALUES(43, 101);"); + + c.Execute("INSERT INTO child VALUES(NULL, 42, 4200);"); + c.Execute("INSERT INTO child VALUES(NULL, 42, 4201);"); + + c.Execute("INSERT INTO child VALUES(NULL, 43, 4300);"); + c.Execute("INSERT INTO child VALUES(NULL, 43, 4301);"); + + // The following command deletes "parent(43, 101)", then in turns + // "child(NULL, 43, 4300/4301)", then calls the MyDelete on 4300 and + // 4301 + c.Execute("DELETE FROM parent WHERE dummy=101"); + + ASSERT_EQ(2u, func->deleted_.size()); + 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; + } +} + + +TEST(SQLite, Types) +{ + SQLite::Connection c; + c.OpenInMemory(); + c.Execute("CREATE TABLE a(id INTEGER PRIMARY KEY, value)"); + + { + SQLite::Statement s(c, std::string("SELECT * FROM a")); + ASSERT_EQ(2, s.ColumnCount()); + ASSERT_FALSE(s.Step()); + } + + { + SQLite::Statement s(c, SQLITE_FROM_HERE, std::string("SELECT * FROM a")); + ASSERT_FALSE(s.Step()); + ASSERT_EQ("SELECT * FROM a", s.GetOriginalSQLStatement()); + } + + { + SQLite::Statement s(c, SQLITE_FROM_HERE, "INSERT INTO a VALUES(NULL, ?);"); + s.BindNull(0); ASSERT_TRUE(s.Run()); s.Reset(); + s.BindBool(0, true); ASSERT_TRUE(s.Run()); s.Reset(); + s.BindInt(0, 42); ASSERT_TRUE(s.Run()); s.Reset(); + s.BindInt64(0, 42ll); ASSERT_TRUE(s.Run()); s.Reset(); + s.BindDouble(0, 42.5); ASSERT_TRUE(s.Run()); s.Reset(); + s.BindCString(0, "Hello"); ASSERT_TRUE(s.Run()); s.Reset(); + s.BindBlob(0, "Hello", 5); ASSERT_TRUE(s.Run()); s.Reset(); + } + + { + SQLite::Statement s(c, SQLITE_FROM_HERE, std::string("SELECT * FROM a")); + ASSERT_TRUE(s.Step()); + ASSERT_EQ(SQLite::COLUMN_TYPE_NULL, s.GetColumnType(1)); + ASSERT_TRUE(s.ColumnIsNull(1)); + ASSERT_TRUE(s.Step()); + ASSERT_EQ(SQLite::COLUMN_TYPE_INTEGER, s.GetColumnType(1)); + ASSERT_TRUE(s.ColumnBool(1)); + ASSERT_TRUE(s.Step()); + ASSERT_EQ(SQLite::COLUMN_TYPE_INTEGER, s.GetColumnType(1)); + ASSERT_EQ(42, s.ColumnInt(1)); + ASSERT_TRUE(s.Step()); + ASSERT_EQ(SQLite::COLUMN_TYPE_INTEGER, s.GetColumnType(1)); + ASSERT_EQ(42ll, s.ColumnInt64(1)); + ASSERT_TRUE(s.Step()); + ASSERT_EQ(SQLite::COLUMN_TYPE_FLOAT, s.GetColumnType(1)); + ASSERT_DOUBLE_EQ(42.5, s.ColumnDouble(1)); + ASSERT_TRUE(s.Step()); + ASSERT_EQ(SQLite::COLUMN_TYPE_TEXT, s.GetColumnType(1)); + ASSERT_EQ("Hello", s.ColumnString(1)); + ASSERT_TRUE(s.Step()); + ASSERT_EQ(SQLite::COLUMN_TYPE_BLOB, s.GetColumnType(1)); + ASSERT_EQ(5, s.ColumnByteLength(1)); + ASSERT_TRUE(!memcmp("Hello", s.ColumnBlob(1), 5)); + + std::string t; + ASSERT_TRUE(s.ColumnBlobAsString(1, &t)); + ASSERT_EQ("Hello", t); + + ASSERT_FALSE(s.Step()); + } +}
--- a/UnitTestsSources/ServerIndexTests.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/UnitTestsSources/ServerIndexTests.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -38,6 +38,7 @@ #include "../OrthancServer/ServerIndex.h" #include "../Core/Uuid.h" #include "../Core/DicomFormat/DicomNullValue.h" +#include "../Core/FileStorage/FilesystemStorage.h" #include <ctype.h> #include <glog/logging.h> @@ -57,6 +58,7 @@ { public: std::vector<std::string> deletedFiles_; + std::vector<std::string> deletedResources_; std::string ancestorId_; ResourceType ancestorType_; @@ -78,7 +80,20 @@ const std::string fileUuid = info.GetUuid(); deletedFiles_.push_back(fileUuid); LOG(INFO) << "A file must be removed: " << fileUuid; - } + } + + virtual void SignalChange(const ServerIndexChange& change) + { + if (change.GetChangeType() == ChangeType_Deleted) + { + deletedResources_.push_back(change.GetPublicId()); + } + + LOG(INFO) << "Change related to resource " << change.GetPublicId() << " of type " + << EnumerationToString(change.GetResourceType()) << ": " + << EnumerationToString(change.GetChangeType()); + } + }; @@ -86,7 +101,7 @@ { protected: std::auto_ptr<ServerIndexListener> listener_; - std::auto_ptr<DatabaseWrapper> index_; + std::auto_ptr<IDatabaseWrapper> index_; DatabaseWrapperTest() { @@ -95,7 +110,18 @@ virtual void SetUp() { listener_.reset(new ServerIndexListener); - index_.reset(new DatabaseWrapper(*listener_)); + + switch (GetParam()) + { + case DatabaseWrapperClass_SQLite: + index_.reset(new DatabaseWrapper()); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + index_->SetListener(*listener_); } virtual void TearDown() @@ -103,6 +129,121 @@ index_.reset(NULL); listener_.reset(NULL); } + + void CheckTableRecordCount(uint32_t expected, const char* table) + { + switch (GetParam()) + { + case DatabaseWrapperClass_SQLite: + { + DatabaseWrapper* sqlite = dynamic_cast<DatabaseWrapper*>(index_.get()); + ASSERT_EQ(expected, sqlite->GetTableRecordCount(table)); + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + void CheckNoParent(int64_t id) + { + std::string s; + + switch (GetParam()) + { + case DatabaseWrapperClass_SQLite: + { + DatabaseWrapper* sqlite = dynamic_cast<DatabaseWrapper*>(index_.get()); + ASSERT_FALSE(sqlite->GetParentPublicId(s, id)); + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + void CheckParentPublicId(const char* expected, int64_t id) + { + std::string s; + + switch (GetParam()) + { + case DatabaseWrapperClass_SQLite: + { + DatabaseWrapper* sqlite = dynamic_cast<DatabaseWrapper*>(index_.get()); + ASSERT_TRUE(sqlite->GetParentPublicId(s, id)); + ASSERT_EQ(expected, s); + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + void CheckNoChild(int64_t id) + { + std::list<std::string> j; + + switch (GetParam()) + { + case DatabaseWrapperClass_SQLite: + { + DatabaseWrapper* sqlite = dynamic_cast<DatabaseWrapper*>(index_.get()); + sqlite->GetChildren(j, id); + ASSERT_EQ(0, j.size()); + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + void CheckOneChild(const char* expected, int64_t id) + { + std::list<std::string> j; + + switch (GetParam()) + { + case DatabaseWrapperClass_SQLite: + { + DatabaseWrapper* sqlite = dynamic_cast<DatabaseWrapper*>(index_.get()); + sqlite->GetChildren(j, id); + ASSERT_EQ(1, j.size()); + ASSERT_EQ(expected, j.front()); + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + void CheckTwoChildren(const char* expected1, + const char* expected2, + int64_t id) + { + std::list<std::string> j; + + switch (GetParam()) + { + case DatabaseWrapperClass_SQLite: + { + DatabaseWrapper* sqlite = dynamic_cast<DatabaseWrapper*>(index_.get()); + sqlite->GetChildren(j, id); + ASSERT_EQ(2, j.size()); + ASSERT_TRUE((expected1 == j.front() && expected2 == j.back()) || + (expected1 == j.back() && expected2 == j.front())); + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + } }; } @@ -141,15 +282,15 @@ ASSERT_EQ(ResourceType_Study, index_->GetResourceType(a[6])); { - Json::Value t; + std::list<std::string> t; index_->GetAllPublicIds(t, ResourceType_Patient); ASSERT_EQ(1u, t.size()); - ASSERT_EQ("a", t[0u].asString()); + ASSERT_EQ("a", t.front()); index_->GetAllPublicIds(t, ResourceType_Series); ASSERT_EQ(1u, t.size()); - ASSERT_EQ("c", t[0u].asString()); + ASSERT_EQ("c", t.front()); index_->GetAllPublicIds(t, ResourceType_Study); ASSERT_EQ(2u, t.size()); @@ -176,14 +317,14 @@ 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); + + CheckNoParent(a[0]); + CheckNoParent(a[6]); + CheckParentPublicId("a", a[1]); + CheckParentPublicId("b", a[2]); + CheckParentPublicId("c", a[3]); + CheckParentPublicId("c", a[4]); + CheckParentPublicId("g", a[5]); std::list<std::string> l; index_->GetChildrenPublicId(l, a[0]); ASSERT_EQ(1u, l.size()); ASSERT_EQ("b", l.front()); @@ -209,7 +350,7 @@ ASSERT_EQ(0u, md.size()); index_->AddAttachment(a[4], FileInfo("my json file", FileContentType_DicomAsJson, 42, "md5", - CompressionType_Zlib, 21, "compressedMD5")); + CompressionType_Zlib, 21, "compressedMD5")); index_->AddAttachment(a[4], FileInfo("my dicom file", FileContentType_Dicom, 42, "md5")); index_->AddAttachment(a[6], FileInfo("world", FileContentType_Dicom, 44, "md5")); index_->SetMetadata(a[4], MetadataType_Instance_RemoteAet, "PINNACLE"); @@ -220,35 +361,46 @@ index_->SetMetadata(a[4], MetadataType_ModifiedFrom, "TUTU"); index_->ListAvailableMetadata(md, a[4]); ASSERT_EQ(2u, md.size()); + + std::map<MetadataType, std::string> md2; + index_->GetAllMetadata(md2, a[4]); + ASSERT_EQ(2u, md2.size()); + ASSERT_EQ("TUTU", md2[MetadataType_ModifiedFrom]); + ASSERT_EQ("PINNACLE", md2[MetadataType_Instance_RemoteAet]); + index_->DeleteMetadata(a[4], MetadataType_ModifiedFrom); index_->ListAvailableMetadata(md, a[4]); ASSERT_EQ(1u, md.size()); ASSERT_EQ(MetadataType_Instance_RemoteAet, md.front()); + index_->GetAllMetadata(md2, a[4]); + ASSERT_EQ(1u, md2.size()); + ASSERT_EQ("PINNACLE", md2[MetadataType_Instance_RemoteAet]); + + 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); + index_->SetMainDicomTag(a[3], DicomTag(0x0010, 0x0010), "PatientName"); int64_t b; ResourceType t; - ASSERT_TRUE(index_->LookupResource("g", b, t)); + ASSERT_TRUE(index_->LookupResource(b, t, "g")); 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")); + + std::string u; + ASSERT_TRUE(index_->LookupMetadata(u, a[4], MetadataType_Instance_RemoteAet)); + ASSERT_EQ("PINNACLE", u); + ASSERT_FALSE(index_->LookupMetadata(u, a[4], MetadataType_Instance_IndexInSeries)); 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_DicomAsJson)); @@ -268,12 +420,15 @@ ASSERT_EQ(CompressionType_None, 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")); + ASSERT_EQ(0u, listener_->deletedResources_.size()); + + CheckTableRecordCount(7, "Resources"); + CheckTableRecordCount(3, "AttachedFiles"); + CheckTableRecordCount(1, "Metadata"); + CheckTableRecordCount(1, "MainDicomTags"); + index_->DeleteResource(a[0]); - + ASSERT_EQ(5u, listener_->deletedResources_.size()); ASSERT_EQ(2u, listener_->deletedFiles_.size()); ASSERT_FALSE(std::find(listener_->deletedFiles_.begin(), listener_->deletedFiles_.end(), @@ -282,14 +437,17 @@ 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")); + CheckTableRecordCount(2, "Resources"); + CheckTableRecordCount(0, "Metadata"); + CheckTableRecordCount(1, "AttachedFiles"); + CheckTableRecordCount(0, "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(7u, listener_->deletedResources_.size()); + + CheckTableRecordCount(0, "Resources"); + CheckTableRecordCount(0, "AttachedFiles"); + CheckTableRecordCount(2, "GlobalProperties"); ASSERT_EQ(3u, listener_->deletedFiles_.size()); ASSERT_FALSE(std::find(listener_->deletedFiles_.begin(), @@ -321,29 +479,14 @@ 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()); - } + CheckTwoChildren("b", "f", a[0]); + CheckTwoChildren("c", "g", a[1]); + CheckTwoChildren("d", "e", a[2]); + CheckNoChild(a[3]); + CheckNoChild(a[4]); + CheckOneChild("h", a[5]); + CheckNoChild(a[6]); + CheckNoChild(a[7]); listener_->Reset(); index_->DeleteResource(a[3]); @@ -374,19 +517,22 @@ 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, - "md5-" + boost::lexical_cast<std::string>(i))); + "md5-" + boost::lexical_cast<std::string>(i))); ASSERT_FALSE(index_->IsProtectedPatient(patients[i])); } - ASSERT_EQ(10u, index_->GetTableRecordCount("Resources")); - ASSERT_EQ(10u, index_->GetTableRecordCount("PatientRecyclingOrder")); + CheckTableRecordCount(10u, "Resources"); + CheckTableRecordCount(10u, "PatientRecyclingOrder"); listener_->Reset(); + ASSERT_EQ(0u, listener_->deletedResources_.size()); index_->DeleteResource(patients[5]); index_->DeleteResource(patients[0]); - ASSERT_EQ(8u, index_->GetTableRecordCount("Resources")); - ASSERT_EQ(8u, index_->GetTableRecordCount("PatientRecyclingOrder")); + ASSERT_EQ(2u, listener_->deletedResources_.size()); + + CheckTableRecordCount(8u, "Resources"); + CheckTableRecordCount(8u, "PatientRecyclingOrder"); ASSERT_EQ(2u, listener_->deletedFiles_.size()); ASSERT_EQ("Patient 5", listener_->deletedFiles_[0]); @@ -395,24 +541,32 @@ int64_t p; ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[1]); index_->DeleteResource(p); + ASSERT_EQ(3u, listener_->deletedResources_.size()); ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]); index_->DeleteResource(p); + ASSERT_EQ(4u, listener_->deletedResources_.size()); ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[3]); index_->DeleteResource(p); + ASSERT_EQ(5u, listener_->deletedResources_.size()); ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]); index_->DeleteResource(p); + ASSERT_EQ(6u, listener_->deletedResources_.size()); ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[6]); index_->DeleteResource(p); index_->DeleteResource(patients[8]); + ASSERT_EQ(8u, listener_->deletedResources_.size()); ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[7]); index_->DeleteResource(p); + ASSERT_EQ(9u, listener_->deletedResources_.size()); ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[9]); index_->DeleteResource(p); ASSERT_FALSE(index_->SelectPatientToRecycle(p)); + ASSERT_EQ(10u, listener_->deletedResources_.size()); ASSERT_EQ(10u, listener_->deletedFiles_.size()); - ASSERT_EQ(0u, index_->GetTableRecordCount("Resources")); - ASSERT_EQ(0u, index_->GetTableRecordCount("PatientRecyclingOrder")); + + CheckTableRecordCount(0, "Resources"); + CheckTableRecordCount(0, "PatientRecyclingOrder"); } @@ -424,88 +578,101 @@ 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, - "md5-" + boost::lexical_cast<std::string>(i))); + "md5-" + boost::lexical_cast<std::string>(i))); ASSERT_FALSE(index_->IsProtectedPatient(patients[i])); } - ASSERT_EQ(5u, index_->GetTableRecordCount("Resources")); - ASSERT_EQ(5u, index_->GetTableRecordCount("PatientRecyclingOrder")); + CheckTableRecordCount(5, "Resources"); + CheckTableRecordCount(5, "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")); + CheckTableRecordCount(5, "Resources"); + CheckTableRecordCount(4, "PatientRecyclingOrder"); index_->SetProtectedPatient(patients[2], true); ASSERT_TRUE(index_->IsProtectedPatient(patients[2])); - ASSERT_EQ(4u, index_->GetTableRecordCount("PatientRecyclingOrder")); + CheckTableRecordCount(4, "PatientRecyclingOrder"); index_->SetProtectedPatient(patients[2], false); ASSERT_FALSE(index_->IsProtectedPatient(patients[2])); - ASSERT_EQ(5u, index_->GetTableRecordCount("PatientRecyclingOrder")); + CheckTableRecordCount(5, "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")); + CheckTableRecordCount(5, "PatientRecyclingOrder"); + CheckTableRecordCount(5, "Resources"); index_->SetProtectedPatient(patients[2], true); ASSERT_TRUE(index_->IsProtectedPatient(patients[2])); - ASSERT_EQ(4u, index_->GetTableRecordCount("PatientRecyclingOrder")); + CheckTableRecordCount(4, "PatientRecyclingOrder"); index_->SetProtectedPatient(patients[2], false); ASSERT_FALSE(index_->IsProtectedPatient(patients[2])); - ASSERT_EQ(5u, index_->GetTableRecordCount("PatientRecyclingOrder")); + CheckTableRecordCount(5, "PatientRecyclingOrder"); index_->SetProtectedPatient(patients[3], true); ASSERT_TRUE(index_->IsProtectedPatient(patients[3])); - ASSERT_EQ(4u, index_->GetTableRecordCount("PatientRecyclingOrder")); + CheckTableRecordCount(4, "PatientRecyclingOrder"); - ASSERT_EQ(5u, index_->GetTableRecordCount("Resources")); + CheckTableRecordCount(5, "Resources"); ASSERT_EQ(0u, listener_->deletedFiles_.size()); // Unprotecting a patient puts it at the last position in the recycling queue int64_t p; + ASSERT_EQ(0u, listener_->deletedResources_.size()); ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[0]); index_->DeleteResource(p); + ASSERT_EQ(1u, listener_->deletedResources_.size()); 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_EQ(2u, listener_->deletedResources_.size()); ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]); index_->DeleteResource(p); + ASSERT_EQ(3u, listener_->deletedResources_.size()); ASSERT_FALSE(index_->SelectPatientToRecycle(p, patients[2])); ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]); index_->DeleteResource(p); + ASSERT_EQ(4u, listener_->deletedResources_.size()); // "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")); + CheckTableRecordCount(1, "Resources"); + CheckTableRecordCount(0, "PatientRecyclingOrder"); index_->SetProtectedPatient(patients[3], false); - ASSERT_EQ(1u, index_->GetTableRecordCount("PatientRecyclingOrder")); + CheckTableRecordCount(1, "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_->deletedResources_.size()); ASSERT_EQ(5u, listener_->deletedFiles_.size()); - ASSERT_EQ(0u, index_->GetTableRecordCount("Resources")); - ASSERT_EQ(0u, index_->GetTableRecordCount("PatientRecyclingOrder")); + CheckTableRecordCount(0, "Resources"); + CheckTableRecordCount(0, "PatientRecyclingOrder"); } -TEST_P(DatabaseWrapperTest, Sequence) +TEST(ServerIndex, Sequence) { - ASSERT_EQ(1u, index_->IncrementGlobalSequence(GlobalProperty_AnonymizationSequence)); - ASSERT_EQ(2u, index_->IncrementGlobalSequence(GlobalProperty_AnonymizationSequence)); - ASSERT_EQ(3u, index_->IncrementGlobalSequence(GlobalProperty_AnonymizationSequence)); - ASSERT_EQ(4u, index_->IncrementGlobalSequence(GlobalProperty_AnonymizationSequence)); + const std::string path = "UnitTestsStorage"; + + Toolbox::RemoveFile(path + "/index"); + FilesystemStorage storage(path); + DatabaseWrapper db; // The SQLite DB is in memory + ServerContext context(db); + context.SetStorageArea(storage); + ServerIndex& index = context.GetIndex(); + + ASSERT_EQ(1u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence)); + ASSERT_EQ(2u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence)); + ASSERT_EQ(3u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence)); + ASSERT_EQ(4u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence)); } -TEST_P(DatabaseWrapperTest, LookupTagValue) +TEST_P(DatabaseWrapperTest, LookupIdentifier) { int64_t a[] = { index_->CreateResource("a", ResourceType_Study), // 0 @@ -514,42 +681,41 @@ index_->CreateResource("d", ResourceType_Series) // 3 }; - DicomMap m; - m.Clear(); m.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "0"); index_->SetMainDicomTags(a[0], m); - m.Clear(); m.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "1"); index_->SetMainDicomTags(a[1], m); - m.Clear(); m.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "0"); index_->SetMainDicomTags(a[2], m); - m.Clear(); m.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, "0"); index_->SetMainDicomTags(a[3], m); + index_->SetMainDicomTag(a[0], DICOM_TAG_STUDY_INSTANCE_UID, "0"); + index_->SetMainDicomTag(a[1], DICOM_TAG_STUDY_INSTANCE_UID, "1"); + index_->SetMainDicomTag(a[2], DICOM_TAG_STUDY_INSTANCE_UID, "0"); + index_->SetMainDicomTag(a[3], DICOM_TAG_SERIES_INSTANCE_UID, "0"); std::list<int64_t> s; - index_->LookupTagValue(s, DICOM_TAG_STUDY_INSTANCE_UID, "0"); + index_->LookupIdentifier(s, DICOM_TAG_STUDY_INSTANCE_UID, "0"); ASSERT_EQ(2u, s.size()); ASSERT_TRUE(std::find(s.begin(), s.end(), a[0]) != s.end()); ASSERT_TRUE(std::find(s.begin(), s.end(), a[2]) != s.end()); - index_->LookupTagValue(s, "0"); + index_->LookupIdentifier(s, "0"); ASSERT_EQ(3u, s.size()); ASSERT_TRUE(std::find(s.begin(), s.end(), a[0]) != s.end()); ASSERT_TRUE(std::find(s.begin(), s.end(), a[2]) != s.end()); ASSERT_TRUE(std::find(s.begin(), s.end(), a[3]) != s.end()); - index_->LookupTagValue(s, DICOM_TAG_STUDY_INSTANCE_UID, "1"); + index_->LookupIdentifier(s, DICOM_TAG_STUDY_INSTANCE_UID, "1"); ASSERT_EQ(1u, s.size()); ASSERT_TRUE(std::find(s.begin(), s.end(), a[1]) != s.end()); - index_->LookupTagValue(s, "1"); + index_->LookupIdentifier(s, "1"); ASSERT_EQ(1u, s.size()); ASSERT_TRUE(std::find(s.begin(), s.end(), a[1]) != s.end()); /*{ - std::list<std::string> s; - context.GetIndex().LookupTagValue(s, DICOM_TAG_STUDY_INSTANCE_UID, "1.2.250.1.74.20130819132500.29000036381059"); - for (std::list<std::string>::iterator i = s.begin(); i != s.end(); i++) - { - std::cout << "*** " << *i << std::endl;; - } - }*/ + std::list<std::string> s; + context.GetIndex().LookupIdentifier(s, DICOM_TAG_STUDY_INSTANCE_UID, "1.2.250.1.74.20130819132500.29000036381059"); + for (std::list<std::string>::iterator i = s.begin(); i != s.end(); i++) + { + std::cout << "*** " << *i << std::endl;; + } + }*/ } @@ -557,8 +723,12 @@ TEST(ServerIndex, AttachmentRecycling) { const std::string path = "UnitTestsStorage"; + Toolbox::RemoveFile(path + "/index"); - ServerContext context(path, ":memory:"); // The SQLite DB is in memory + FilesystemStorage storage(path); + DatabaseWrapper db; // The SQLite DB is in memory + ServerContext context(db); + context.SetStorageArea(storage); ServerIndex& index = context.GetIndex(); index.SetMaximumStorageSize(10); @@ -579,7 +749,13 @@ instance.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "study-" + id); instance.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, "series-" + id); instance.SetValue(DICOM_TAG_SOP_INSTANCE_UID, "instance-" + id); - ASSERT_EQ(StoreStatus_Success, index.Store(instance, attachments, "")); + + std::map<MetadataType, std::string> instanceMetadata; + ServerIndex::MetadataMap metadata; + ASSERT_EQ(StoreStatus_Success, index.Store(instanceMetadata, instance, attachments, "", metadata)); + ASSERT_EQ(2, instanceMetadata.size()); + ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_RemoteAet) != instanceMetadata.end()); + ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_ReceptionDate) != instanceMetadata.end()); DicomInstanceHasher hasher(instance); ids.push_back(hasher.HashPatient());
--- a/UnitTestsSources/UnitTestsMain.cpp Wed Jun 25 15:34:40 2014 +0200 +++ b/UnitTestsSources/UnitTestsMain.cpp Thu May 21 16:58:30 2015 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 @@ -45,6 +45,7 @@ #include "../Core/Uuid.h" #include "../OrthancServer/OrthancInitialization.h" + using namespace Orthanc; @@ -174,45 +175,96 @@ } -TEST(ParseGetQuery, Basic) +TEST(ParseGetArguments, Basic) { + HttpHandler::GetArguments b; + HttpHandler::ParseGetArguments(b, "aaa=baaa&bb=a&aa=c"); + HttpHandler::Arguments a; - HttpHandler::ParseGetQuery(a, "aaa=baaa&bb=a&aa=c"); + HttpHandler::CompileGetArguments(a, b); + ASSERT_EQ(3u, a.size()); ASSERT_EQ(a["aaa"], "baaa"); ASSERT_EQ(a["bb"], "a"); ASSERT_EQ(a["aa"], "c"); } -TEST(ParseGetQuery, BasicEmpty) +TEST(ParseGetArguments, BasicEmpty) { + HttpHandler::GetArguments b; + HttpHandler::ParseGetArguments(b, "aaa&bb=aa&aa"); + HttpHandler::Arguments a; - HttpHandler::ParseGetQuery(a, "aaa&bb=aa&aa"); + HttpHandler::CompileGetArguments(a, b); + ASSERT_EQ(3u, a.size()); ASSERT_EQ(a["aaa"], ""); ASSERT_EQ(a["bb"], "aa"); ASSERT_EQ(a["aa"], ""); } -TEST(ParseGetQuery, Single) +TEST(ParseGetArguments, Single) { + HttpHandler::GetArguments b; + HttpHandler::ParseGetArguments(b, "aaa=baaa"); + HttpHandler::Arguments a; - HttpHandler::ParseGetQuery(a, "aaa=baaa"); + HttpHandler::CompileGetArguments(a, b); + ASSERT_EQ(1u, a.size()); ASSERT_EQ(a["aaa"], "baaa"); } -TEST(ParseGetQuery, SingleEmpty) +TEST(ParseGetArguments, SingleEmpty) { + HttpHandler::GetArguments b; + HttpHandler::ParseGetArguments(b, "aaa"); + HttpHandler::Arguments a; - HttpHandler::ParseGetQuery(a, "aaa"); + HttpHandler::CompileGetArguments(a, b); + ASSERT_EQ(1u, a.size()); ASSERT_EQ(a["aaa"], ""); } +TEST(ParseGetQuery, Test1) +{ + UriComponents uri; + HttpHandler::GetArguments b; + HttpHandler::ParseGetQuery(uri, b, "/instances/test/world?aaa=baaa&bb=a&aa=c"); + + HttpHandler::Arguments a; + HttpHandler::CompileGetArguments(a, b); + + ASSERT_EQ(3u, uri.size()); + ASSERT_EQ("instances", uri[0]); + ASSERT_EQ("test", uri[1]); + ASSERT_EQ("world", uri[2]); + ASSERT_EQ(3u, a.size()); + ASSERT_EQ(a["aaa"], "baaa"); + ASSERT_EQ(a["bb"], "a"); + ASSERT_EQ(a["aa"], "c"); +} + +TEST(ParseGetQuery, Test2) +{ + UriComponents uri; + HttpHandler::GetArguments b; + HttpHandler::ParseGetQuery(uri, b, "/instances/test/world"); + + HttpHandler::Arguments a; + HttpHandler::CompileGetArguments(a, b); + + ASSERT_EQ(3u, uri.size()); + ASSERT_EQ("instances", uri[0]); + ASSERT_EQ("test", uri[1]); + ASSERT_EQ("world", uri[2]); + ASSERT_EQ(0u, a.size()); +} + TEST(Uri, SplitUriComponents) { - UriComponents c; + UriComponents c, d; Toolbox::SplitUriComponents(c, "/cou/hello/world"); ASSERT_EQ(3u, c.size()); ASSERT_EQ("cou", c[0]); @@ -253,6 +305,37 @@ } +TEST(Uri, Truncate) +{ + UriComponents c, d; + Toolbox::SplitUriComponents(c, "/cou/hello/world"); + + Toolbox::TruncateUri(d, c, 0); + ASSERT_EQ(3u, d.size()); + ASSERT_EQ("cou", d[0]); + ASSERT_EQ("hello", d[1]); + ASSERT_EQ("world", d[2]); + + Toolbox::TruncateUri(d, c, 1); + ASSERT_EQ(2u, d.size()); + ASSERT_EQ("hello", d[0]); + ASSERT_EQ("world", d[1]); + + Toolbox::TruncateUri(d, c, 2); + ASSERT_EQ(1u, d.size()); + ASSERT_EQ("world", d[0]); + + Toolbox::TruncateUri(d, c, 3); + ASSERT_EQ(0u, d.size()); + + Toolbox::TruncateUri(d, c, 4); + ASSERT_EQ(0u, d.size()); + + Toolbox::TruncateUri(d, c, 5); + ASSERT_EQ(0u, d.size()); +} + + TEST(Uri, Child) { UriComponents c1; Toolbox::SplitUriComponents(c1, "/hello/world"); @@ -581,23 +664,58 @@ ASSERT_EQ("", t[3]); } +TEST(Toolbox, Enumerations) +{ + ASSERT_EQ(Encoding_Utf8, StringToEncoding(EnumerationToString(Encoding_Utf8))); + ASSERT_EQ(Encoding_Ascii, StringToEncoding(EnumerationToString(Encoding_Ascii))); + ASSERT_EQ(Encoding_Latin1, StringToEncoding(EnumerationToString(Encoding_Latin1))); + ASSERT_EQ(Encoding_Latin2, StringToEncoding(EnumerationToString(Encoding_Latin2))); + ASSERT_EQ(Encoding_Latin3, StringToEncoding(EnumerationToString(Encoding_Latin3))); + ASSERT_EQ(Encoding_Latin4, StringToEncoding(EnumerationToString(Encoding_Latin4))); + ASSERT_EQ(Encoding_Latin5, StringToEncoding(EnumerationToString(Encoding_Latin5))); + ASSERT_EQ(Encoding_Cyrillic, StringToEncoding(EnumerationToString(Encoding_Cyrillic))); + ASSERT_EQ(Encoding_Arabic, StringToEncoding(EnumerationToString(Encoding_Arabic))); + ASSERT_EQ(Encoding_Greek, StringToEncoding(EnumerationToString(Encoding_Greek))); + ASSERT_EQ(Encoding_Hebrew, StringToEncoding(EnumerationToString(Encoding_Hebrew))); + ASSERT_EQ(Encoding_Japanese, StringToEncoding(EnumerationToString(Encoding_Japanese))); + ASSERT_EQ(Encoding_Chinese, StringToEncoding(EnumerationToString(Encoding_Chinese))); + ASSERT_EQ(Encoding_Thai, StringToEncoding(EnumerationToString(Encoding_Thai))); + + ASSERT_EQ(ResourceType_Patient, StringToResourceType(EnumerationToString(ResourceType_Patient))); + ASSERT_EQ(ResourceType_Study, StringToResourceType(EnumerationToString(ResourceType_Study))); + ASSERT_EQ(ResourceType_Series, StringToResourceType(EnumerationToString(ResourceType_Series))); + ASSERT_EQ(ResourceType_Instance, StringToResourceType(EnumerationToString(ResourceType_Instance))); + + ASSERT_EQ(ImageFormat_Png, StringToImageFormat(EnumerationToString(ImageFormat_Png))); +} + #if defined(__linux) #include <endian.h> +#elif defined(__FreeBSD__) +#include <machine/endian.h> #endif + TEST(Toolbox, Endianness) { // Parts of this test come from Adam Conrad // http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=728822#5 -#if defined(_WIN32) + + /** + * Windows and OS X are assumed to always little-endian. + **/ + +#if defined(_WIN32) || defined(__APPLE__) ASSERT_EQ(Endianness_Little, Toolbox::DetectEndianness()); -#elif defined(__APPLE__) - ASSERT_EQ(Endianness_Little, Toolbox::DetectEndianness()); + /** + * Linux. + **/ + #elif defined(__linux) || defined(__FreeBSD_kernel__) #if !defined(__BYTE_ORDER) @@ -610,12 +728,67 @@ ASSERT_EQ(Endianness_Little, Toolbox::DetectEndianness()); # endif + + /** + * FreeBSD. + **/ + +#elif defined(__FreeBSD__) +# if _BYTE_ORDER == _BIG_ENDIAN + ASSERT_EQ(Endianness_Big, Toolbox::DetectEndianness()); +# else // _LITTLE_ENDIAN + ASSERT_EQ(Endianness_Little, Toolbox::DetectEndianness()); +# endif + #else #error Support your platform here #endif } +#if ORTHANC_PUGIXML_ENABLED == 1 +TEST(Toolbox, Xml) +{ + Json::Value a; + a["hello"] = "world"; + a["42"] = 43; + a["b"] = Json::arrayValue; + a["b"].append("test"); + a["b"].append("test2"); + + std::string s; + Toolbox::JsonToXml(s, a); + + std::cout << s; +} +#endif + + +#if !defined(_WIN32) +TEST(Toolbox, ExecuteSystemCommand) +{ + std::vector<std::string> args(2); + args[0] = "Hello"; + args[1] = "World"; + + Toolbox::ExecuteSystemCommand("echo", args); +} +#endif + + +TEST(Toolbox, IsInteger) +{ + ASSERT_TRUE(Toolbox::IsInteger("00236")); + ASSERT_TRUE(Toolbox::IsInteger("-0042")); + ASSERT_TRUE(Toolbox::IsInteger("0")); + ASSERT_TRUE(Toolbox::IsInteger("-0")); + + ASSERT_FALSE(Toolbox::IsInteger("")); + ASSERT_FALSE(Toolbox::IsInteger("42a")); + ASSERT_FALSE(Toolbox::IsInteger("42-")); +} + + int main(int argc, char **argv) { // Initialize Google's logging library.
--- a/UnitTestsSources/Versions.cpp Wed Jun 25 15:34:40 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,133 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - **/ - - -#include "PrecompiledHeadersUnitTests.h" -#include "gtest/gtest.h" - -#include <stdint.h> -#include <math.h> -#include <png.h> -#include <ctype.h> -#include <zlib.h> -#include <curl/curl.h> -#include <boost/version.hpp> -#include <sqlite3.h> -#include <lua.h> -#include <openssl/opensslv.h> - - -TEST(Versions, Zlib) -{ - ASSERT_STREQ(zlibVersion(), ZLIB_VERSION); -} - -TEST(Versions, Curl) -{ - curl_version_info_data* v = curl_version_info(CURLVERSION_NOW); - ASSERT_STREQ(LIBCURL_VERSION, v->version); -} - -TEST(Versions, Png) -{ - ASSERT_EQ(PNG_LIBPNG_VER_MAJOR * 10000 + PNG_LIBPNG_VER_MINOR * 100 + PNG_LIBPNG_VER_RELEASE, - 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); -} - - -TEST(Versions, Lua) -{ - // Ensure that the Lua version is above 5.1.0. This version has - // introduced some API changes. - ASSERT_GE(LUA_VERSION_NUM, 501); -} - - -#if ORTHANC_STATIC == 1 -TEST(Versions, ZlibStatic) -{ - ASSERT_STREQ("1.2.7", zlibVersion()); -} - -TEST(Versions, BoostStatic) -{ - ASSERT_STREQ("1_55", BOOST_LIB_VERSION); -} - -TEST(Versions, CurlStatic) -{ - curl_version_info_data* v = curl_version_info(CURLVERSION_NOW); - ASSERT_STREQ("7.26.0", v->version); -} - -TEST(Versions, PngStatic) -{ - ASSERT_EQ(10512, png_access_version_number()); - ASSERT_STREQ("1.5.12", PNG_LIBPNG_VER_STRING); -} - -TEST(Versions, CurlSslStatic) -{ - curl_version_info_data * vinfo = curl_version_info(CURLVERSION_NOW); - - // Check that SSL support is enabled when required - bool curlSupportsSsl = vinfo->features & CURL_VERSION_SSL; - -#if ORTHANC_SSL_ENABLED == 0 - ASSERT_FALSE(curlSupportsSsl); -#else - ASSERT_TRUE(curlSupportsSsl); -#endif -} - -TEST(Version, LuaStatic) -{ - ASSERT_STREQ("Lua 5.1.5", LUA_RELEASE); -} - -TEST(Version, OpenSslStatic) -{ - ASSERT_EQ(0x1000107fL /* openssl-1.0.1g */, OPENSSL_VERSION_NUMBER); -} - -#endif
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/UnitTestsSources/VersionsTests.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,133 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "PrecompiledHeadersUnitTests.h" +#include "gtest/gtest.h" + +#include <stdint.h> +#include <math.h> +#include <png.h> +#include <ctype.h> +#include <zlib.h> +#include <curl/curl.h> +#include <boost/version.hpp> +#include <sqlite3.h> +#include <lua.h> +#include <openssl/opensslv.h> + + +TEST(Versions, Zlib) +{ + ASSERT_STREQ(zlibVersion(), ZLIB_VERSION); +} + +TEST(Versions, Curl) +{ + curl_version_info_data* v = curl_version_info(CURLVERSION_NOW); + ASSERT_STREQ(LIBCURL_VERSION, v->version); +} + +TEST(Versions, Png) +{ + ASSERT_EQ(PNG_LIBPNG_VER_MAJOR * 10000 + PNG_LIBPNG_VER_MINOR * 100 + PNG_LIBPNG_VER_RELEASE, + 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); +} + + +TEST(Versions, Lua) +{ + // Ensure that the Lua version is above 5.1.0. This version has + // introduced some API changes. + ASSERT_GE(LUA_VERSION_NUM, 501); +} + + +#if ORTHANC_STATIC == 1 +TEST(Versions, ZlibStatic) +{ + ASSERT_STREQ("1.2.7", zlibVersion()); +} + +TEST(Versions, BoostStatic) +{ + ASSERT_STREQ("1_55", BOOST_LIB_VERSION); +} + +TEST(Versions, CurlStatic) +{ + curl_version_info_data* v = curl_version_info(CURLVERSION_NOW); + ASSERT_STREQ("7.26.0", v->version); +} + +TEST(Versions, PngStatic) +{ + ASSERT_EQ(10512, png_access_version_number()); + ASSERT_STREQ("1.5.12", PNG_LIBPNG_VER_STRING); +} + +TEST(Versions, CurlSslStatic) +{ + curl_version_info_data * vinfo = curl_version_info(CURLVERSION_NOW); + + // Check that SSL support is enabled when required + bool curlSupportsSsl = vinfo->features & CURL_VERSION_SSL; + +#if ORTHANC_SSL_ENABLED == 0 + ASSERT_FALSE(curlSupportsSsl); +#else + ASSERT_TRUE(curlSupportsSsl); +#endif +} + +TEST(Version, LuaStatic) +{ + ASSERT_STREQ("Lua 5.1.5", LUA_RELEASE); +} + +TEST(Version, OpenSslStatic) +{ + ASSERT_EQ(0x1000107fL /* openssl-1.0.1g */, OPENSSL_VERSION_NUMBER); +} + +#endif
--- a/UnitTestsSources/Zip.cpp Wed Jun 25 15:34:40 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,166 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege, - * Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - **/ - - -#include "PrecompiledHeadersUnitTests.h" -#include "gtest/gtest.h" - -#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("UnitTestsResults/hello.zip"); - w.Open(); - w.OpenFile("world/hello"); - w.Write("Hello world"); -} - - -TEST(ZipWriter, Basic64) -{ - Orthanc::ZipWriter w; - w.SetOutputPath("UnitTestsResults/hello64.zip"); - w.SetZip64(true); - w.Open(); - w.OpenFile("world/hello"); - w.Write("Hello world"); -} - - -TEST(ZipWriter, Exceptions) -{ - Orthanc::ZipWriter w; - ASSERT_THROW(w.Open(), Orthanc::OrthancException); - w.SetOutputPath("UnitTestsResults/hello3.zip"); - 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("UnitTestsResults/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. - - **/ -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/UnitTestsSources/ZipTests.cpp Thu May 21 16:58:30 2015 +0200 @@ -0,0 +1,188 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital 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 "PrecompiledHeadersUnitTests.h" +#include "gtest/gtest.h" + +#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("UnitTestsResults/hello.zip"); + w.Open(); + w.OpenFile("world/hello"); + w.Write("Hello world"); +} + + +TEST(ZipWriter, Basic64) +{ + Orthanc::ZipWriter w; + w.SetOutputPath("UnitTestsResults/hello64.zip"); + w.SetZip64(true); + w.Open(); + w.OpenFile("world/hello"); + w.Write("Hello world"); +} + + +TEST(ZipWriter, Exceptions) +{ + Orthanc::ZipWriter w; + ASSERT_THROW(w.Open(), Orthanc::OrthancException); + w.SetOutputPath("UnitTestsResults/hello3.zip"); + w.Open(); + ASSERT_THROW(w.Write("hello world"), Orthanc::OrthancException); +} + + +TEST(ZipWriter, Append) +{ + { + Orthanc::ZipWriter w; + w.SetAppendToExisting(false); + w.SetOutputPath("UnitTestsResults/append.zip"); + w.Open(); + w.OpenFile("world/hello"); + w.Write("Hello world 1"); + } + + { + Orthanc::ZipWriter w; + w.SetAppendToExisting(true); + w.SetOutputPath("UnitTestsResults/append.zip"); + w.Open(); + w.OpenFile("world/appended"); + w.Write("Hello world 2"); + } +} + + + + + +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("UnitTestsResults/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. + + **/ +}