# HG changeset patch
# User Sebastien Jodogne
# Date 1355480549 -3600
# Node ID ffd98d2f0b911c6f54bfb064c50da74217af08dc
# Parent 40d3bf6cc8d913ccb0371061f5c2ce44cec5aea0# Parent 9cd240cfd3a6561abcc480d3caff1684788be4cb
merge
diff -r 9cd240cfd3a6 -r ffd98d2f0b91 CMakeLists.txt
--- a/CMakeLists.txt Mon Dec 10 11:00:50 2012 +0100
+++ b/CMakeLists.txt Fri Dec 14 11:22:29 2012 +0100
@@ -4,7 +4,7 @@
# Version of the build, should always be "mainline" except in release branches
add_definitions(
- -DORTHANC_VERSION="0.3.1"
+ -DORTHANC_VERSION="mainline"
)
# Parameters of the build
@@ -99,6 +99,7 @@
${AUTOGENERATED_SOURCES}
${THIRD_PARTY_SOURCES}
+ Core/Cache/MemoryCache.cpp
Core/ChunkedBuffer.cpp
Core/Compression/BufferCompressor.cpp
Core/Compression/ZlibCompressor.cpp
@@ -182,7 +183,6 @@
include(${CMAKE_SOURCE_DIR}/Resources/CMake/GoogleTestConfiguration.cmake)
add_executable(UnitTests
${GTEST_SOURCES}
- UnitTests/MessageWithDestination.cpp
UnitTests/RestApi.cpp
UnitTests/SQLite.cpp
UnitTests/SQLiteChromium.cpp
@@ -190,6 +190,7 @@
UnitTests/Versions.cpp
UnitTests/Zip.cpp
UnitTests/FileStorage.cpp
+ UnitTests/MemoryCache.cpp
UnitTests/main.cpp
)
target_link_libraries(UnitTests ServerLibrary CoreLibrary)
diff -r 9cd240cfd3a6 -r ffd98d2f0b91 Core/Cache/CacheIndex.h
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Cache/CacheIndex.h Fri Dec 14 11:22:29 2012 +0100
@@ -0,0 +1,250 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012 Medical Physics Department, CHU of Liege,
+ * Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ **/
+
+
+#pragma once
+
+#include
+#include
diff -r 9cd240cfd3a6 -r ffd98d2f0b91 OrthancExplorer/explorer.js
--- a/OrthancExplorer/explorer.js Mon Dec 10 11:00:50 2012 +0100
+++ b/OrthancExplorer/explorer.js Fri Dec 14 11:22:29 2012 +0100
@@ -378,6 +378,18 @@
}
target.listview('refresh');
+
+ // Check whether this patient is protected
+ $.ajax({
+ url: '../patients/' + $.mobile.pageData.uuid + '/protected',
+ type: 'GET',
+ dataType: 'text',
+ async: false,
+ success: function (s) {
+ var v = (s == '1') ? 'on' : 'off';
+ $('#protection').val(v).slider('refresh');
+ }
+ });
});
});
}
@@ -786,3 +798,13 @@
window.location.href = '../series/' + $.mobile.pageData.uuid + '/archive';
});
+$('#protection').live('change', function(e) {
+ var isProtected = e.target.value == "on";
+ $.ajax({
+ url: '../patients/' + $.mobile.pageData.uuid + '/protected',
+ type: 'PUT',
+ dataType: 'text',
+ data: isProtected ? '1' : '0',
+ async: false
+ });
+});
diff -r 9cd240cfd3a6 -r ffd98d2f0b91 OrthancServer/DatabaseWrapper.cpp
--- a/OrthancServer/DatabaseWrapper.cpp Mon Dec 10 11:00:50 2012 +0100
+++ b/OrthancServer/DatabaseWrapper.cpp Fri Dec 14 11:22:29 2012 +0100
@@ -33,6 +33,7 @@
#include "DatabaseWrapper.h"
#include "../Core/DicomFormat/DicomArray.h"
+#include "../Core/Uuid.h"
#include "EmbeddedResources.h"
#include
@@ -61,12 +62,18 @@
virtual unsigned int GetCardinality() const
{
- return 1;
+ return 5;
}
virtual void Compute(SQLite::FunctionContext& context)
{
- listener_.SignalFileDeleted(context.GetStringValue(0));
+ FileInfo info(context.GetStringValue(0),
+ static_cast(context.GetIntValue(1)),
+ static_cast(context.GetInt64Value(2)),
+ static_cast(context.GetIntValue(3)),
+ static_cast(context.GetInt64Value(4)));
+
+ listener_.SignalFileDeleted(info);
}
};
@@ -743,9 +750,9 @@
LOG(INFO) << "Version of the Orthanc database: " << version;
unsigned int v = boost::lexical_cast(version);
- // This version of Orthanc is only compatible with version 2 of
- // the DB schema (since Orthanc 0.3.1)
- ok = (v == 2);
+ // This version of Orthanc is only compatible with version 3 of
+ // the DB schema (since Orthanc 0.3.2)
+ ok = (v == 3);
}
catch (boost::bad_lexical_cast&)
{
@@ -777,4 +784,70 @@
return c;
}
+
+ bool DatabaseWrapper::SelectPatientToRecycle(int64_t& internalId)
+ {
+ SQLite::Statement s(db_, SQLITE_FROM_HERE,
+ "SELECT patientId FROM PatientRecyclingOrder ORDER BY seq ASC LIMIT 1");
+
+ if (!s.Step())
+ {
+ // No patient remaining or all the patients are protected
+ return false;
+ }
+ else
+ {
+ internalId = s.ColumnInt(0);
+ return true;
+ }
+ }
+
+ bool DatabaseWrapper::SelectPatientToRecycle(int64_t& internalId,
+ int64_t patientIdToAvoid)
+ {
+ SQLite::Statement s(db_, SQLITE_FROM_HERE,
+ "SELECT patientId FROM PatientRecyclingOrder "
+ "WHERE patientId != ? ORDER BY seq ASC LIMIT 1");
+ s.BindInt(0, patientIdToAvoid);
+
+ if (!s.Step())
+ {
+ // No patient remaining or all the patients are protected
+ return false;
+ }
+ else
+ {
+ internalId = s.ColumnInt(0);
+ return true;
+ }
+ }
+
+ bool DatabaseWrapper::IsProtectedPatient(int64_t internalId)
+ {
+ SQLite::Statement s(db_, SQLITE_FROM_HERE,
+ "SELECT * FROM PatientRecyclingOrder WHERE patientId = ?");
+ s.BindInt(0, internalId);
+ return !s.Step();
+ }
+
+ void DatabaseWrapper::SetProtectedPatient(int64_t internalId,
+ bool isProtected)
+ {
+ if (isProtected)
+ {
+ SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM PatientRecyclingOrder WHERE patientId=?");
+ s.BindInt(0, internalId);
+ s.Run();
+ }
+ else if (IsProtectedPatient(internalId))
+ {
+ SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)");
+ s.BindInt(0, internalId);
+ s.Run();
+ }
+ else
+ {
+ // Nothing to do: The patient is already unprotected
+ }
+ }
}
diff -r 9cd240cfd3a6 -r ffd98d2f0b91 OrthancServer/DatabaseWrapper.h
--- a/OrthancServer/DatabaseWrapper.h Mon Dec 10 11:00:50 2012 +0100
+++ b/OrthancServer/DatabaseWrapper.h Fri Dec 14 11:22:29 2012 +0100
@@ -179,6 +179,16 @@
void GetAllPublicIds(Json::Value& target,
ResourceType resourceType);
+ bool SelectPatientToRecycle(int64_t& internalId);
+
+ bool SelectPatientToRecycle(int64_t& internalId,
+ int64_t patientIdToAvoid);
+
+ bool IsProtectedPatient(int64_t internalId);
+
+ void SetProtectedPatient(int64_t internalId,
+ bool isProtected);
+
DatabaseWrapper(const std::string& path,
IServerIndexListener& listener);
diff -r 9cd240cfd3a6 -r ffd98d2f0b91 OrthancServer/FromDcmtkBridge.cpp
--- a/OrthancServer/FromDcmtkBridge.cpp Mon Dec 10 11:00:50 2012 +0100
+++ b/OrthancServer/FromDcmtkBridge.cpp Fri Dec 14 11:22:29 2012 +0100
@@ -63,6 +63,193 @@
namespace Orthanc
{
+ ParsedDicomFile::ParsedDicomFile(const std::string& content)
+ {
+ DcmInputBufferStream is;
+ if (content.size() > 0)
+ {
+ is.setBuffer(&content[0], content.size());
+ }
+ is.setEos();
+
+ file_.reset(new DcmFileFormat);
+ if (!file_->read(is).good())
+ {
+ throw OrthancException(ErrorCode_BadFileFormat);
+ }
+ }
+
+
+ static void SendPathValueForDictionary(RestApiOutput& output,
+ DcmItem& dicom)
+ {
+ Json::Value v = Json::arrayValue;
+
+ for (unsigned long i = 0; i < dicom.card(); i++)
+ {
+ DcmElement* element = dicom.getElement(i);
+ if (element)
+ {
+ char buf[16];
+ sprintf(buf, "%04x-%04x", element->getTag().getGTag(), element->getTag().getETag());
+ v.append(buf);
+ }
+ }
+
+ output.AnswerJson(v);
+ }
+
+ static inline uint16_t GetCharValue(char c)
+ {
+ if (c >= '0' && c <= '9')
+ return c - '0';
+ else if (c >= 'a' && c <= 'f')
+ return c - 'a' + 10;
+ else if (c >= 'A' && c <= 'F')
+ return c - 'A' + 10;
+ else
+ return 0;
+ }
+
+ static inline uint16_t GetTagValue(const char* c)
+ {
+ return ((GetCharValue(c[0]) << 12) +
+ (GetCharValue(c[1]) << 8) +
+ (GetCharValue(c[2]) << 4) +
+ GetCharValue(c[3]));
+ }
+
+ static bool ParseTagAndGroup(DcmTagKey& key,
+ const std::string& tag)
+ {
+ if (tag.size() != 9 ||
+ !isxdigit(tag[0]) ||
+ !isxdigit(tag[1]) ||
+ !isxdigit(tag[2]) ||
+ !isxdigit(tag[3]) ||
+ tag[4] != '-' ||
+ !isxdigit(tag[5]) ||
+ !isxdigit(tag[6]) ||
+ !isxdigit(tag[7]) ||
+ !isxdigit(tag[8]))
+ {
+ return false;
+ }
+
+ uint16_t group = GetTagValue(tag.c_str());
+ uint16_t element = GetTagValue(tag.c_str() + 5);
+
+ key = DcmTagKey(group, element);
+
+ return true;
+ }
+
+ static void SendPathValueForLeaf(RestApiOutput& output,
+ const std::string& tag,
+ DcmItem& dicom)
+ {
+ DcmTagKey k;
+ if (!ParseTagAndGroup(k, tag))
+ {
+ return;
+ }
+
+ DcmElement* element = NULL;
+ if (dicom.findAndGetElement(k, element).good() && element != NULL)
+ {
+ if (element->getVR() == EVR_SQ)
+ {
+ // This element is a sequence
+ Json::Value v = Json::arrayValue;
+ DcmSequenceOfItems& sequence = dynamic_cast(*element);
+
+ for (unsigned long i = 0; i < sequence.card(); i++)
+ {
+ v.append(boost::lexical_cast(i));
+ }
+
+ output.AnswerJson(v);
+ }
+ else
+ {
+ // This element is not a sequence
+ std::string buffer;
+ buffer.resize(65536);
+ Uint32 length = element->getLength();
+ Uint32 offset = 0;
+
+ output.GetLowLevelOutput().SendOkHeader("application/octet-stream", true, length, NULL);
+
+ while (offset < length)
+ {
+ Uint32 nbytes;
+ if (length - offset < buffer.size())
+ {
+ nbytes = length - offset;
+ }
+ else
+ {
+ nbytes = buffer.size();
+ }
+
+ if (element->getPartialValue(&buffer[0], offset, nbytes).good())
+ {
+ output.GetLowLevelOutput().Send(&buffer[0], nbytes);
+ offset += nbytes;
+ }
+ else
+ {
+ return;
+ }
+ }
+
+ output.MarkLowLevelOutputDone();
+ }
+ }
+ }
+
+ void ParsedDicomFile::SendPathValue(RestApiOutput& output,
+ const UriComponents& uri)
+ {
+ DcmItem* dicom = file_->getDataset();
+
+ // Go down in the tag hierarchy according to the URI
+ for (size_t pos = 0; pos < uri.size() / 2; pos++)
+ {
+ size_t index;
+ try
+ {
+ index = boost::lexical_cast(uri[2 * pos + 1]);
+ }
+ catch (boost::bad_lexical_cast&)
+ {
+ return;
+ }
+
+ DcmTagKey k;
+ DcmItem *child = NULL;
+ if (!ParseTagAndGroup(k, uri[2 * pos]) ||
+ !dicom->findAndGetSequenceItem(k, child, index).good() ||
+ child == NULL)
+ {
+ return;
+ }
+
+ dicom = child;
+ }
+
+ // We have reached the end of the URI
+ if (uri.size() % 2 == 0)
+ {
+ SendPathValueForDictionary(output, *dicom);
+ }
+ else
+ {
+ SendPathValueForLeaf(output, uri.back(), *dicom);
+ }
+ }
+
+
void FromDcmtkBridge::Convert(DicomMap& target, DcmDataset& dataset)
{
target.Clear();
diff -r 9cd240cfd3a6 -r ffd98d2f0b91 OrthancServer/FromDcmtkBridge.h
--- a/OrthancServer/FromDcmtkBridge.h Mon Dec 10 11:00:50 2012 +0100
+++ b/OrthancServer/FromDcmtkBridge.h Fri Dec 14 11:22:29 2012 +0100
@@ -33,8 +33,13 @@
#pragma once
#include "../Core/DicomFormat/DicomMap.h"
+#include "../Core/RestApi/RestApiOutput.h"
+#include "../Core/Toolbox.h"
+
#include
+#include
#include
+#include
namespace Orthanc
{
@@ -52,6 +57,23 @@
DicomRootLevel_Instance
};
+ class ParsedDicomFile : public IDynamicObject
+ {
+ private:
+ std::auto_ptr file_;
+
+ public:
+ ParsedDicomFile(const std::string& content);
+
+ DcmFileFormat& GetDicom()
+ {
+ return *file_;
+ }
+
+ void SendPathValue(RestApiOutput& output,
+ const UriComponents& uri);
+ };
+
class FromDcmtkBridge
{
public:
diff -r 9cd240cfd3a6 -r ffd98d2f0b91 OrthancServer/IServerIndexListener.h
--- a/OrthancServer/IServerIndexListener.h Mon Dec 10 11:00:50 2012 +0100
+++ b/OrthancServer/IServerIndexListener.h Fri Dec 14 11:22:29 2012 +0100
@@ -47,7 +47,6 @@
virtual void SignalRemainingAncestor(ResourceType parentType,
const std::string& publicId) = 0;
- virtual void SignalFileDeleted(const std::string& fileUuid) = 0;
-
+ virtual void SignalFileDeleted(const FileInfo& info) = 0;
};
}
diff -r 9cd240cfd3a6 -r ffd98d2f0b91 OrthancServer/OrthancInitialization.h
--- a/OrthancServer/OrthancInitialization.h Mon Dec 10 11:00:50 2012 +0100
+++ b/OrthancServer/OrthancInitialization.h Fri Dec 14 11:22:29 2012 +0100
@@ -35,6 +35,7 @@
#include
#include
#include
+#include
#include "../Core/HttpServer/MongooseServer.h"
namespace Orthanc
diff -r 9cd240cfd3a6 -r ffd98d2f0b91 OrthancServer/OrthancRestApi.cpp
--- a/OrthancServer/OrthancRestApi.cpp Mon Dec 10 11:00:50 2012 +0100
+++ b/OrthancServer/OrthancRestApi.cpp Fri Dec 14 11:22:29 2012 +0100
@@ -607,6 +607,40 @@
}
+ // Get information about a single patient -----------------------------------
+
+ static void IsProtectedPatient(RestApi::GetCall& call)
+ {
+ RETRIEVE_CONTEXT(call);
+ std::string publicId = call.GetUriComponent("id", "");
+ bool isProtected = context.GetIndex().IsProtectedPatient(publicId);
+ call.GetOutput().AnswerBuffer(isProtected ? "1" : "0", "text/plain");
+ }
+
+
+ static void SetPatientProtection(RestApi::PutCall& call)
+ {
+ RETRIEVE_CONTEXT(call);
+ std::string publicId = call.GetUriComponent("id", "");
+ std::string s = Toolbox::StripSpaces(call.GetPutBody());
+
+ if (s == "0")
+ {
+ context.GetIndex().SetProtectedPatient(publicId, false);
+ call.GetOutput().AnswerBuffer("", "text/plain");
+ }
+ else if (s == "1")
+ {
+ context.GetIndex().SetProtectedPatient(publicId, true);
+ call.GetOutput().AnswerBuffer("", "text/plain");
+ }
+ else
+ {
+ // Bad request
+ }
+ }
+
+
// Get information about a single instance ----------------------------------
static void GetInstanceFile(RestApi::GetCall& call)
@@ -813,6 +847,23 @@
+ // Raw access to the DICOM tags of an instance ------------------------------
+
+ static void GetRawContent(RestApi::GetCall& call)
+ {
+ // TODO IMPROVE MULTITHREADING
+ static boost::mutex mutex_;
+ boost::mutex::scoped_lock lock(mutex_);
+
+ RETRIEVE_CONTEXT(call);
+ std::string id = call.GetUriComponent("id", "");
+ ParsedDicomFile& dicom = context.GetDicomFile(id);
+ dicom.SendPathValue(call.GetOutput(), call.GetTrailingUri());
+ }
+
+
+
+
// Registration of the various REST handlers --------------------------------
OrthancRestApi::OrthancRestApi(ServerContext& context) :
@@ -845,10 +896,13 @@
Register("/studies/{id}/archive", GetArchive);
Register("/series/{id}/archive", GetArchive);
+ Register("/patients/{id}/protected", IsProtectedPatient);
+ Register("/patients/{id}/protected", SetPatientProtection);
Register("/instances/{id}/file", GetInstanceFile);
Register("/instances/{id}/tags", GetInstanceTags);
Register("/instances/{id}/simplified-tags", GetInstanceTags);
Register("/instances/{id}/frames", ListFrames);
+ Register("/instances/{id}/content/*", GetRawContent);
Register("/instances/{id}/frames/{frame}/preview", GetImage);
Register("/instances/{id}/frames/{frame}/image-uint8", GetImage);
diff -r 9cd240cfd3a6 -r ffd98d2f0b91 OrthancServer/PrepareDatabase.sql
--- a/OrthancServer/PrepareDatabase.sql Mon Dec 10 11:00:50 2012 +0100
+++ b/OrthancServer/PrepareDatabase.sql Fri Dec 14 11:22:29 2012 +0100
@@ -55,9 +55,15 @@
date TEXT
);
+CREATE TABLE PatientRecyclingOrder(
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
+ patientId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE
+ );
+
CREATE INDEX ChildrenIndex ON Resources(parentId);
CREATE INDEX PublicIndex ON Resources(publicId);
CREATE INDEX ResourceTypeIndex ON Resources(resourceType);
+CREATE INDEX PatientRecyclingIndex ON PatientRecyclingOrder(patientId);
CREATE INDEX MainDicomTagsIndex1 ON MainDicomTags(id);
CREATE INDEX MainDicomTagsIndex2 ON MainDicomTags(tagGroup, tagElement);
@@ -68,7 +74,8 @@
CREATE TRIGGER AttachedFileDeleted
AFTER DELETE ON AttachedFiles
BEGIN
- SELECT SignalFileDeleted(old.uuid);
+ SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize,
+ old.compressionType, old.compressedSize);
END;
CREATE TRIGGER ResourceDeleted
@@ -86,6 +93,14 @@
DELETE FROM Resources WHERE internalId = old.parentId;
END;
+CREATE TRIGGER PatientAdded
+AFTER INSERT ON Resources
+FOR EACH ROW WHEN new.resourceType = 1 -- "1" corresponds to "ResourceType_Patient" in C++
+BEGIN
+ INSERT INTO PatientRecyclingOrder VALUES (NULL, new.internalId);
+END;
+
+
-- Set the version of the database schema
-- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration
-INSERT INTO GlobalProperties VALUES (1, "2");
+INSERT INTO GlobalProperties VALUES (1, "3");
diff -r 9cd240cfd3a6 -r ffd98d2f0b91 OrthancServer/ServerContext.cpp
--- a/OrthancServer/ServerContext.cpp Mon Dec 10 11:00:50 2012 +0100
+++ b/OrthancServer/ServerContext.cpp Fri Dec 14 11:22:29 2012 +0100
@@ -37,6 +37,8 @@
#include
+static const size_t DICOM_CACHE_SIZE = 2;
+
/**
* IMPORTANT: We make the assumption that the same instance of
* FileStorage can be accessed from multiple threads. This seems OK
@@ -51,7 +53,9 @@
ServerContext::ServerContext(const boost::filesystem::path& path) :
storage_(path.string()),
index_(*this, path.string()),
- accessor_(storage_)
+ accessor_(storage_),
+ provider_(*this),
+ dicomCache_(provider_, DICOM_CACHE_SIZE)
{
}
@@ -162,4 +166,18 @@
accessor_.SetCompressionForNextOperations(attachment.GetCompressionType());
accessor_.Read(result, attachment.GetUuid());
}
+
+
+ IDynamicObject* ServerContext::DicomCacheProvider::Provide(const std::string& instancePublicId)
+ {
+ std::string content;
+ context_.ReadFile(content, instancePublicId, FileContentType_Dicom);
+ return new ParsedDicomFile(content);
+ }
+
+
+ ParsedDicomFile& ServerContext::GetDicomFile(const std::string& instancePublicId)
+ {
+ return dynamic_cast(dicomCache_.Access(instancePublicId));
+ }
}
diff -r 9cd240cfd3a6 -r ffd98d2f0b91 OrthancServer/ServerContext.h
--- a/OrthancServer/ServerContext.h Mon Dec 10 11:00:50 2012 +0100
+++ b/OrthancServer/ServerContext.h Fri Dec 14 11:22:29 2012 +0100
@@ -32,10 +32,12 @@
#pragma once
-#include "ServerIndex.h"
+#include "../Core/Cache/MemoryCache.h"
#include "../Core/FileStorage/CompressedFileStorageAccessor.h"
#include "../Core/FileStorage/FileStorage.h"
#include "../Core/RestApi/RestApiOutput.h"
+#include "ServerIndex.h"
+#include "FromDcmtkBridge.h"
namespace Orthanc
{
@@ -47,10 +49,26 @@
class ServerContext
{
private:
+ class DicomCacheProvider : public ICachePageProvider
+ {
+ private:
+ ServerContext& context_;
+
+ public:
+ DicomCacheProvider(ServerContext& context) : context_(context)
+ {
+ }
+
+ virtual IDynamicObject* Provide(const std::string& id);
+ };
+
FileStorage storage_;
ServerIndex index_;
CompressedFileStorageAccessor accessor_;
bool compressionEnabled_;
+
+ DicomCacheProvider provider_;
+ MemoryCache dicomCache_;
public:
ServerContext(const boost::filesystem::path& path);
@@ -86,5 +104,8 @@
void ReadFile(std::string& result,
const std::string& instancePublicId,
FileContentType content);
+
+ // TODO IMPLEMENT MULTITHREADING FOR THIS METHOD
+ ParsedDicomFile& GetDicomFile(const std::string& instancePublicId);
};
}
diff -r 9cd240cfd3a6 -r ffd98d2f0b91 OrthancServer/ServerIndex.cpp
--- a/OrthancServer/ServerIndex.cpp Mon Dec 10 11:00:50 2012 +0100
+++ b/OrthancServer/ServerIndex.cpp Fri Dec 14 11:22:29 2012 +0100
@@ -59,12 +59,14 @@
bool hasRemainingLevel_;
ResourceType remainingType_;
std::string remainingPublicId_;
+ std::list pendingFilesToRemove_;
+ uint64_t sizeOfFilesToRemove_;
public:
ServerIndexListener(ServerContext& context) :
- context_(context),
- hasRemainingLevel_(false)
+ context_(context)
{
+ Reset();
assert(ResourceType_Patient < ResourceType_Study &&
ResourceType_Study < ResourceType_Series &&
ResourceType_Series < ResourceType_Instance);
@@ -72,7 +74,24 @@
void Reset()
{
+ sizeOfFilesToRemove_ = 0;
hasRemainingLevel_ = false;
+ pendingFilesToRemove_.clear();
+ }
+
+ uint64_t GetSizeOfFilesToRemove()
+ {
+ return sizeOfFilesToRemove_;
+ }
+
+ void CommitFilesToRemove()
+ {
+ for (std::list::iterator
+ it = pendingFilesToRemove_.begin();
+ it != pendingFilesToRemove_.end(); it++)
+ {
+ context_.RemoveFile(*it);
+ }
}
virtual void SignalRemainingAncestor(ResourceType parentType,
@@ -96,10 +115,11 @@
}
}
- virtual void SignalFileDeleted(const std::string& fileUuid)
+ virtual void SignalFileDeleted(const FileInfo& info)
{
- assert(Toolbox::IsUuid(fileUuid));
- context_.RemoveFile(fileUuid);
+ assert(Toolbox::IsUuid(info.GetUuid()));
+ pendingFilesToRemove_.push_back(info.GetUuid());
+ sizeOfFilesToRemove_ += info.GetCompressedSize();
}
bool HasRemainingLevel() const
@@ -122,16 +142,57 @@
}
+ class ServerIndex::Transaction
+ {
+ private:
+ ServerIndex& index_;
+ std::auto_ptr transaction_;
+ bool isCommitted_;
+
+ public:
+ Transaction(ServerIndex& index) :
+ index_(index),
+ isCommitted_(false)
+ {
+ assert(index_.currentStorageSize_ == index_.db_->GetTotalCompressedSize());
+
+ index_.listener_->Reset();
+ transaction_.reset(index_.db_->StartTransaction());
+ transaction_->Begin();
+ }
+
+ void Commit(uint64_t sizeOfAddedFiles)
+ {
+ if (!isCommitted_)
+ {
+ transaction_->Commit();
+
+ // We can remove the files once the SQLite transaction has
+ // been successfully committed. Some files might have to be
+ // deleted because of recycling.
+ index_.listener_->CommitFilesToRemove();
+
+ index_.currentStorageSize_ += sizeOfAddedFiles;
+
+ assert(index_.currentStorageSize_ >= index_.listener_->GetSizeOfFilesToRemove());
+ index_.currentStorageSize_ -= index_.listener_->GetSizeOfFilesToRemove();
+
+ assert(index_.currentStorageSize_ == index_.db_->GetTotalCompressedSize());
+
+ isCommitted_ = true;
+ }
+ }
+ };
+
+
bool ServerIndex::DeleteResource(Json::Value& target,
const std::string& uuid,
ResourceType expectedType)
{
boost::mutex::scoped_lock lock(mutex_);
-
listener_->Reset();
- std::auto_ptr t(db_->StartTransaction());
- t->Begin();
+ Transaction t(*this);
int64_t id;
ResourceType type;
@@ -158,7 +219,7 @@
target["RemainingAncestor"] = Json::nullValue;
}
- t->Commit();
+ t.Commit(0);
return true;
}
@@ -180,7 +241,9 @@
ServerIndex::ServerIndex(ServerContext& context,
- const std::string& dbPath) : mutex_()
+ const std::string& dbPath) :
+ maximumStorageSize_(0),
+ maximumPatients_(0)
{
listener_.reset(new Internals::ServerIndexListener(context));
@@ -203,6 +266,12 @@
db_.reset(new DatabaseWrapper(p.string() + "/index", *listener_));
}
+ currentStorageSize_ = db_->GetTotalCompressedSize();
+
+ // Initial recycling if the parameters have changed since the last
+ // execution of Orthanc
+ StandaloneRecycling();
+
unsigned int sleep;
try
{
@@ -232,13 +301,13 @@
const std::string& remoteAet)
{
boost::mutex::scoped_lock lock(mutex_);
+ listener_->Reset();
DicomInstanceHasher hasher(dicomSummary);
try
{
- std::auto_ptr t(db_->StartTransaction());
- t->Begin();
+ Transaction t(*this);
int64_t patient, study, series, instance;
ResourceType type;
@@ -251,6 +320,16 @@
return StoreStatus_AlreadyStored;
}
+ // Ensure there is enough room in the storage for the new instance
+ uint64_t instanceSize = 0;
+ for (Attachments::const_iterator it = attachments.begin();
+ it != attachments.end(); it++)
+ {
+ instanceSize += it->GetCompressedSize();
+ }
+
+ Recycle(instanceSize, hasher.HashPatient());
+
// Create the instance
instance = db_->CreateResource(hasher.HashInstance(), ResourceType_Instance);
@@ -337,13 +416,14 @@
db_->LogChange(ChangeType_CompletedSeries, series, ResourceType_Series);
}
- t->Commit();
+ t.Commit(instanceSize);
return StoreStatus_Success;
}
catch (OrthancException& e)
{
- LOG(ERROR) << "EXCEPTION2 [" << e.What() << "]" << " " << db_->GetErrorMessage();
+ LOG(ERROR) << "EXCEPTION [" << e.What() << "]"
+ << " (SQLite status: " << db_->GetErrorMessage() << ")";
}
return StoreStatus_Failure;
@@ -357,7 +437,8 @@
boost::mutex::scoped_lock lock(mutex_);
target = Json::objectValue;
- uint64_t cs = db_->GetTotalCompressedSize();
+ uint64_t cs = currentStorageSize_;
+ assert(cs == db_->GetTotalCompressedSize());
uint64_t us = db_->GetTotalUncompressedSize();
target["TotalDiskSpace"] = boost::lexical_cast(cs);
target["TotalUncompressedSize"] = boost::lexical_cast(us);
@@ -477,20 +558,20 @@
switch (type)
{
- case ResourceType_Study:
- result["ParentPatient"] = parent;
- break;
+ case ResourceType_Study:
+ result["ParentPatient"] = parent;
+ break;
- case ResourceType_Series:
- result["ParentStudy"] = parent;
- break;
+ case ResourceType_Series:
+ result["ParentStudy"] = parent;
+ break;
- case ResourceType_Instance:
- result["ParentSeries"] = parent;
- break;
+ case ResourceType_Instance:
+ result["ParentSeries"] = parent;
+ break;
- default:
- throw OrthancException(ErrorCode_InternalError);
+ default:
+ throw OrthancException(ErrorCode_InternalError);
}
}
@@ -510,72 +591,72 @@
switch (type)
{
- case ResourceType_Patient:
- result["Studies"] = c;
- break;
+ case ResourceType_Patient:
+ result["Studies"] = c;
+ break;
- case ResourceType_Study:
- result["Series"] = c;
- break;
+ case ResourceType_Study:
+ result["Series"] = c;
+ break;
- case ResourceType_Series:
- result["Instances"] = c;
- break;
+ case ResourceType_Series:
+ result["Instances"] = c;
+ break;
- default:
- throw OrthancException(ErrorCode_InternalError);
+ default:
+ throw OrthancException(ErrorCode_InternalError);
}
}
// Set the resource type
switch (type)
{
- case ResourceType_Patient:
- result["Type"] = "Patient";
- break;
+ case ResourceType_Patient:
+ result["Type"] = "Patient";
+ break;
- case ResourceType_Study:
- result["Type"] = "Study";
- break;
-
- case ResourceType_Series:
- {
- result["Type"] = "Series";
- result["Status"] = ToString(GetSeriesStatus(id));
+ case ResourceType_Study:
+ result["Type"] = "Study";
+ break;
- int i;
- if (db_->GetMetadataAsInteger(i, id, MetadataType_Series_ExpectedNumberOfInstances))
- result["ExpectedNumberOfInstances"] = i;
- else
- result["ExpectedNumberOfInstances"] = Json::nullValue;
-
- break;
- }
+ case ResourceType_Series:
+ {
+ result["Type"] = "Series";
+ result["Status"] = ToString(GetSeriesStatus(id));
- case ResourceType_Instance:
- {
- result["Type"] = "Instance";
+ int i;
+ if (db_->GetMetadataAsInteger(i, id, MetadataType_Series_ExpectedNumberOfInstances))
+ result["ExpectedNumberOfInstances"] = i;
+ else
+ result["ExpectedNumberOfInstances"] = Json::nullValue;
- FileInfo attachment;
- if (!db_->LookupAttachment(attachment, id, FileContentType_Dicom))
- {
- throw OrthancException(ErrorCode_InternalError);
+ break;
}
- result["FileSize"] = static_cast(attachment.GetUncompressedSize());
- result["FileUuid"] = attachment.GetUuid();
+ case ResourceType_Instance:
+ {
+ result["Type"] = "Instance";
+
+ FileInfo attachment;
+ if (!db_->LookupAttachment(attachment, id, FileContentType_Dicom))
+ {
+ throw OrthancException(ErrorCode_InternalError);
+ }
- int i;
- if (db_->GetMetadataAsInteger(i, id, MetadataType_Instance_IndexInSeries))
- result["IndexInSeries"] = i;
- else
- result["IndexInSeries"] = Json::nullValue;
+ result["FileSize"] = static_cast(attachment.GetUncompressedSize());
+ result["FileUuid"] = attachment.GetUuid();
- break;
- }
+ int i;
+ if (db_->GetMetadataAsInteger(i, id, MetadataType_Instance_IndexInSeries))
+ result["IndexInSeries"] = i;
+ else
+ result["IndexInSeries"] = Json::nullValue;
- default:
- throw OrthancException(ErrorCode_InternalError);
+ break;
+ }
+
+ default:
+ throw OrthancException(ErrorCode_InternalError);
}
// Record the remaining information
@@ -666,28 +747,28 @@
switch (currentType)
{
- case ResourceType_Patient:
- patientId = map.GetValue(DICOM_TAG_PATIENT_ID).AsString();
- done = true;
- break;
+ case ResourceType_Patient:
+ patientId = map.GetValue(DICOM_TAG_PATIENT_ID).AsString();
+ done = true;
+ break;
- case ResourceType_Study:
- studyInstanceUid = map.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString();
- currentType = ResourceType_Patient;
- break;
+ case ResourceType_Study:
+ studyInstanceUid = map.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString();
+ currentType = ResourceType_Patient;
+ break;
- case ResourceType_Series:
- seriesInstanceUid = map.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).AsString();
- currentType = ResourceType_Study;
- break;
+ case ResourceType_Series:
+ seriesInstanceUid = map.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).AsString();
+ currentType = ResourceType_Study;
+ break;
- case ResourceType_Instance:
- sopInstanceUid = map.GetValue(DICOM_TAG_SOP_INSTANCE_UID).AsString();
- currentType = ResourceType_Series;
- break;
+ case ResourceType_Instance:
+ sopInstanceUid = map.GetValue(DICOM_TAG_SOP_INSTANCE_UID).AsString();
+ currentType = ResourceType_Series;
+ break;
- default:
- throw OrthancException(ErrorCode_InternalError);
+ default:
+ throw OrthancException(ErrorCode_InternalError);
}
// If we have not reached the Patient level, find the parent of
@@ -724,4 +805,161 @@
db_->GetLastExportedResource(target);
return true;
}
+
+
+ bool ServerIndex::IsRecyclingNeeded(uint64_t instanceSize)
+ {
+ if (maximumStorageSize_ != 0)
+ {
+ uint64_t currentSize = currentStorageSize_ - listener_->GetSizeOfFilesToRemove();
+ assert(db_->GetTotalCompressedSize() == currentSize);
+
+ if (currentSize + instanceSize > maximumStorageSize_)
+ {
+ return true;
+ }
+ }
+
+ if (maximumPatients_ != 0)
+ {
+ uint64_t patientCount = db_->GetResourceCount(ResourceType_Patient);
+ if (patientCount > maximumPatients_)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+
+ void ServerIndex::Recycle(uint64_t instanceSize,
+ const std::string& newPatientId)
+ {
+ if (!IsRecyclingNeeded(instanceSize))
+ {
+ return;
+ }
+
+ // Check whether other DICOM instances from this patient are
+ // already stored
+ int64_t patientToAvoid;
+ ResourceType type;
+ bool hasPatientToAvoid = db_->LookupResource(newPatientId, patientToAvoid, type);
+
+ if (hasPatientToAvoid && type != ResourceType_Patient)
+ {
+ throw OrthancException(ErrorCode_InternalError);
+ }
+
+ // Iteratively select patient to remove until there is enough
+ // space in the DICOM store
+ int64_t patientToRecycle;
+ while (true)
+ {
+ // If other instances of this patient are already in the store,
+ // we must avoid to recycle them
+ bool ok = hasPatientToAvoid ?
+ db_->SelectPatientToRecycle(patientToRecycle, patientToAvoid) :
+ db_->SelectPatientToRecycle(patientToRecycle);
+
+ if (!ok)
+ {
+ throw OrthancException(ErrorCode_FullStorage);
+ }
+
+ LOG(INFO) << "Recycling one patient";
+ db_->DeleteResource(patientToRecycle);
+
+ if (!IsRecyclingNeeded(instanceSize))
+ {
+ // OK, we're done
+ break;
+ }
+ }
+ }
+
+ void ServerIndex::SetMaximumPatientCount(unsigned int count)
+ {
+ boost::mutex::scoped_lock lock(mutex_);
+ maximumPatients_ = count;
+
+ if (count == 0)
+ {
+ LOG(WARNING) << "No limit on the number of stored patients";
+ }
+ else
+ {
+ LOG(WARNING) << "At most " << count << " patients will be stored";
+ }
+
+ StandaloneRecycling();
+ }
+
+ void ServerIndex::SetMaximumStorageSize(uint64_t size)
+ {
+ boost::mutex::scoped_lock lock(mutex_);
+ maximumStorageSize_ = size;
+
+ if (size == 0)
+ {
+ LOG(WARNING) << "No limit on the size of the storage area";
+ }
+ else
+ {
+ LOG(WARNING) << "At most " << (size / (1024 * 1024)) << "MB will be used for the storage area";
+ }
+
+ StandaloneRecycling();
+ }
+
+ void ServerIndex::StandaloneRecycling()
+ {
+ // WARNING: No mutex here, do not include this as a public method
+ Transaction t(*this);
+ Recycle(0, "");
+ t.Commit(0);
+ }
+
+
+ bool ServerIndex::IsProtectedPatient(const std::string& publicId)
+ {
+ boost::mutex::scoped_lock lock(mutex_);
+
+ // Lookup for the requested resource
+ int64_t id;
+ ResourceType type;
+ if (!db_->LookupResource(publicId, id, type) ||
+ type != ResourceType_Patient)
+ {
+ throw OrthancException(ErrorCode_ParameterOutOfRange);
+ }
+
+ return db_->IsProtectedPatient(id);
+ }
+
+
+ void ServerIndex::SetProtectedPatient(const std::string& publicId,
+ bool isProtected)
+ {
+ boost::mutex::scoped_lock lock(mutex_);
+
+ // Lookup for the requested resource
+ int64_t id;
+ ResourceType type;
+ if (!db_->LookupResource(publicId, id, type) ||
+ type != ResourceType_Patient)
+ {
+ throw OrthancException(ErrorCode_ParameterOutOfRange);
+ }
+
+ // No need for a SQLite::Transaction here, as we only make 1 write to the DB
+ db_->SetProtectedPatient(id, isProtected);
+
+ if (isProtected)
+ LOG(INFO) << "Patient " << publicId << " has been protected";
+ else
+ LOG(INFO) << "Patient " << publicId << " has been unprotected";
+ }
+
}
diff -r 9cd240cfd3a6 -r ffd98d2f0b91 OrthancServer/ServerIndex.h
--- a/OrthancServer/ServerIndex.h Mon Dec 10 11:00:50 2012 +0100
+++ b/OrthancServer/ServerIndex.h Fri Dec 14 11:22:29 2012 +0100
@@ -51,22 +51,33 @@
class ServerIndexListener;
}
-
-
class ServerIndex : public boost::noncopyable
{
private:
+ class Transaction;
+
boost::mutex mutex_;
boost::thread flushThread_;
std::auto_ptr listener_;
std::auto_ptr db_;
+ uint64_t currentStorageSize_;
+ uint64_t maximumStorageSize_;
+ unsigned int maximumPatients_;
+
void MainDicomTagsToJson(Json::Value& result,
int64_t resourceId);
SeriesStatus GetSeriesStatus(int id);
+ bool IsRecyclingNeeded(uint64_t instanceSize);
+
+ void Recycle(uint64_t instanceSize,
+ const std::string& newPatientId);
+
+ void StandaloneRecycling();
+
public:
typedef std::list Attachments;
@@ -75,6 +86,22 @@
~ServerIndex();
+ uint64_t GetMaximumStorageSize() const
+ {
+ return maximumStorageSize_;
+ }
+
+ uint64_t GetMaximumPatientCount() const
+ {
+ return maximumPatients_;
+ }
+
+ // "size == 0" means no limit on the storage size
+ void SetMaximumStorageSize(uint64_t size);
+
+ // "count == 0" means no limit on the number of patients
+ void SetMaximumPatientCount(unsigned int count);
+
StoreStatus Store(const DicomMap& dicomSummary,
const Attachments& attachments,
const std::string& remoteAet);
@@ -111,5 +138,9 @@
bool GetLastExportedResource(Json::Value& target);
+ bool IsProtectedPatient(const std::string& publicId);
+
+ void SetProtectedPatient(const std::string& publicId,
+ bool isProtected);
};
}
diff -r 9cd240cfd3a6 -r ffd98d2f0b91 OrthancServer/main.cpp
--- a/OrthancServer/main.cpp Mon Dec 10 11:00:50 2012 +0100
+++ b/OrthancServer/main.cpp Fri Dec 14 11:22:29 2012 +0100
@@ -214,6 +214,25 @@
ServerContext context(storageDirectory);
context.SetCompressionEnabled(GetGlobalBoolParameter("StorageCompression", false));
+ try
+ {
+ context.GetIndex().SetMaximumPatientCount(GetGlobalIntegerParameter("MaximumPatientCount", 0));
+ }
+ catch (...)
+ {
+ context.GetIndex().SetMaximumPatientCount(0);
+ }
+
+ try
+ {
+ uint64_t size = GetGlobalIntegerParameter("MaximumStorageSize", 0);
+ context.GetIndex().SetMaximumStorageSize(size * 1024 * 1024);
+ }
+ catch (...)
+ {
+ context.GetIndex().SetMaximumStorageSize(0);
+ }
+
MyDicomStoreFactory storeScp(context);
{
diff -r 9cd240cfd3a6 -r ffd98d2f0b91 Resources/Archives/MessageWithDestination.cpp
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Archives/MessageWithDestination.cpp Fri Dec 14 11:22:29 2012 +0100
@@ -0,0 +1,171 @@
+#include "../Core/IDynamicObject.h"
+
+#include "../Core/OrthancException.h"
+
+#include
+#include
+#include