# HG changeset patch
# User Sebastien Jodogne
# Date 1355061797 -3600
# Node ID 58f96993372082d677dc322c3737b651405adb11
# Parent 8af8754a7a8e44df7e18f4e8daab7d3ec514c607# Parent 3b3525dee6619f7332fc2cfa017fb6c3f2d5b3d4
merge with Orthanc-0.3.1
diff -r 3b3525dee661 -r 58f969933720 CMakeLists.txt
--- a/CMakeLists.txt Sun Dec 09 15:01:00 2012 +0100
+++ b/CMakeLists.txt Sun Dec 09 15:03:17 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
diff -r 3b3525dee661 -r 58f969933720 Core/Enumerations.h
--- a/Core/Enumerations.h Sun Dec 09 15:01:00 2012 +0100
+++ b/Core/Enumerations.h Sun Dec 09 15:03:17 2012 +0100
@@ -55,7 +55,8 @@
ErrorCode_BadFileFormat,
ErrorCode_Timeout,
ErrorCode_UnknownResource,
- ErrorCode_IncompatibleDatabaseVersion
+ ErrorCode_IncompatibleDatabaseVersion,
+ ErrorCode_FullStorage
};
enum PixelFormat
diff -r 3b3525dee661 -r 58f969933720 Core/OrthancException.cpp
--- a/Core/OrthancException.cpp Sun Dec 09 15:01:00 2012 +0100
+++ b/Core/OrthancException.cpp Sun Dec 09 15:03:17 2012 +0100
@@ -93,6 +93,9 @@
case ErrorCode_IncompatibleDatabaseVersion:
return "Incompatible version of the database";
+ case ErrorCode_FullStorage:
+ return "The file storage is full";
+
case ErrorCode_Custom:
default:
return "???";
diff -r 3b3525dee661 -r 58f969933720 Core/SQLite/FunctionContext.cpp
--- a/Core/SQLite/FunctionContext.cpp Sun Dec 09 15:01:00 2012 +0100
+++ b/Core/SQLite/FunctionContext.cpp Sun Dec 09 15:03:17 2012 +0100
@@ -72,6 +72,12 @@
return sqlite3_value_int(argv_[index]);
}
+ int64_t FunctionContext::GetInt64Value(unsigned int index) const
+ {
+ CheckIndex(index);
+ return sqlite3_value_int64(argv_[index]);
+ }
+
double FunctionContext::GetDoubleValue(unsigned int index) const
{
CheckIndex(index);
diff -r 3b3525dee661 -r 58f969933720 Core/SQLite/FunctionContext.h
--- a/Core/SQLite/FunctionContext.h Sun Dec 09 15:01:00 2012 +0100
+++ b/Core/SQLite/FunctionContext.h Sun Dec 09 15:03:17 2012 +0100
@@ -69,6 +69,8 @@
int GetIntValue(unsigned int index) const;
+ int64_t GetInt64Value(unsigned int index) const;
+
double GetDoubleValue(unsigned int index) const;
std::string GetStringValue(unsigned int index) const;
diff -r 3b3525dee661 -r 58f969933720 NEWS
--- a/NEWS Sun Dec 09 15:01:00 2012 +0100
+++ b/NEWS Sun Dec 09 15:03:17 2012 +0100
@@ -1,6 +1,8 @@
Pending changes in the mainline
===============================
+* Recycling of disk space
+* Protection of patients against recycling (also in Orthanc Explorer)
Version 0.3.1 (2012/12/05)
diff -r 3b3525dee661 -r 58f969933720 OrthancExplorer/explorer.css
--- a/OrthancExplorer/explorer.css Sun Dec 09 15:01:00 2012 +0100
+++ b/OrthancExplorer/explorer.css Sun Dec 09 15:03:17 2012 +0100
@@ -37,3 +37,7 @@
text-decoration: none;
color: white !important;
}
+
+.switch-container .ui-slider-switch {
+ width: 100%;
+}
\ No newline at end of file
diff -r 3b3525dee661 -r 58f969933720 OrthancExplorer/explorer.html
--- a/OrthancExplorer/explorer.html Sun Dec 09 15:01:00 2012 +0100
+++ b/OrthancExplorer/explorer.html Sun Dec 09 15:03:17 2012 +0100
@@ -82,7 +82,13 @@
- Go to patient finder
+
+
+
+
Delete this patient
Download ZIP
diff -r 3b3525dee661 -r 58f969933720 OrthancExplorer/explorer.js
--- a/OrthancExplorer/explorer.js Sun Dec 09 15:01:00 2012 +0100
+++ b/OrthancExplorer/explorer.js Sun Dec 09 15:03:17 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 3b3525dee661 -r 58f969933720 OrthancServer/DatabaseWrapper.cpp
--- a/OrthancServer/DatabaseWrapper.cpp Sun Dec 09 15:01:00 2012 +0100
+++ b/OrthancServer/DatabaseWrapper.cpp Sun Dec 09 15:03:17 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 3b3525dee661 -r 58f969933720 OrthancServer/DatabaseWrapper.h
--- a/OrthancServer/DatabaseWrapper.h Sun Dec 09 15:01:00 2012 +0100
+++ b/OrthancServer/DatabaseWrapper.h Sun Dec 09 15:03:17 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 3b3525dee661 -r 58f969933720 OrthancServer/IServerIndexListener.h
--- a/OrthancServer/IServerIndexListener.h Sun Dec 09 15:01:00 2012 +0100
+++ b/OrthancServer/IServerIndexListener.h Sun Dec 09 15:03:17 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 3b3525dee661 -r 58f969933720 OrthancServer/OrthancInitialization.h
--- a/OrthancServer/OrthancInitialization.h Sun Dec 09 15:01:00 2012 +0100
+++ b/OrthancServer/OrthancInitialization.h Sun Dec 09 15:03:17 2012 +0100
@@ -35,6 +35,7 @@
#include
#include
#include
+#include
#include "../Core/HttpServer/MongooseServer.h"
namespace Orthanc
diff -r 3b3525dee661 -r 58f969933720 OrthancServer/OrthancRestApi.cpp
--- a/OrthancServer/OrthancRestApi.cpp Sun Dec 09 15:01:00 2012 +0100
+++ b/OrthancServer/OrthancRestApi.cpp Sun Dec 09 15:03:17 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)
@@ -845,6 +879,8 @@
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);
diff -r 3b3525dee661 -r 58f969933720 OrthancServer/PrepareDatabase.sql
--- a/OrthancServer/PrepareDatabase.sql Sun Dec 09 15:01:00 2012 +0100
+++ b/OrthancServer/PrepareDatabase.sql Sun Dec 09 15:03:17 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 3b3525dee661 -r 58f969933720 OrthancServer/ServerContext.cpp
--- a/OrthancServer/ServerContext.cpp Sun Dec 09 15:01:00 2012 +0100
+++ b/OrthancServer/ServerContext.cpp Sun Dec 09 15:03:17 2012 +0100
@@ -53,6 +53,9 @@
index_(*this, path.string()),
accessor_(storage_)
{
+ // TODO RECYCLING SETUP HERE
+ //index_.SetMaximumPatientCount(4);
+ //index_.SetMaximumStorageSize(10);
}
void ServerContext::SetCompressionEnabled(bool enabled)
diff -r 3b3525dee661 -r 58f969933720 OrthancServer/ServerIndex.cpp
--- a/OrthancServer/ServerIndex.cpp Sun Dec 09 15:01:00 2012 +0100
+++ b/OrthancServer/ServerIndex.cpp Sun Dec 09 15:03:17 2012 +0100
@@ -59,6 +59,7 @@
bool hasRemainingLevel_;
ResourceType remainingType_;
std::string remainingPublicId_;
+ std::list pendingFilesToRemove_;
public:
ServerIndexListener(ServerContext& context) :
@@ -73,6 +74,17 @@
void Reset()
{
hasRemainingLevel_ = false;
+ pendingFilesToRemove_.clear();
+ }
+
+ void CommitFilesToRemove()
+ {
+ for (std::list::iterator
+ it = pendingFilesToRemove_.begin();
+ it != pendingFilesToRemove_.end(); it++)
+ {
+ context_.RemoveFile(*it);
+ }
}
virtual void SignalRemainingAncestor(ResourceType parentType,
@@ -96,10 +108,10 @@
}
}
- 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());
}
bool HasRemainingLevel() const
@@ -127,7 +139,6 @@
ResourceType expectedType)
{
boost::mutex::scoped_lock lock(mutex_);
-
listener_->Reset();
std::auto_ptr t(db_->StartTransaction());
@@ -160,6 +171,10 @@
t->Commit();
+ // We can remove the files once the SQLite transaction has been
+ // successfully committed
+ listener_->CommitFilesToRemove();
+
return true;
}
@@ -180,7 +195,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 +220,10 @@
db_.reset(new DatabaseWrapper(p.string() + "/index", *listener_));
}
+ // Initial recycling if the parameters have changed since the last
+ // execution of Orthanc
+ StandaloneRecycling();
+
unsigned int sleep;
try
{
@@ -232,6 +253,7 @@
const std::string& remoteAet)
{
boost::mutex::scoped_lock lock(mutex_);
+ listener_->Reset();
DicomInstanceHasher hasher(dicomSummary);
@@ -251,6 +273,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);
@@ -339,11 +371,17 @@
t->Commit();
+ // We can remove the files once the SQLite transaction has been
+ // successfully committed. Some files might have to be deleted
+ // because of recycling.
+ listener_->CommitFilesToRemove();
+
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;
@@ -477,20 +515,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 +548,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 +704,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 +762,161 @@
db_->GetLastExportedResource(target);
return true;
}
+
+
+ bool ServerIndex::IsRecyclingNeeded(uint64_t instanceSize)
+ {
+ if (maximumStorageSize_ != 0)
+ {
+ uint64_t currentSize = db_->GetTotalCompressedSize();
+ 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
+ std::auto_ptr t(db_->StartTransaction());
+ t->Begin();
+ Recycle(0, "");
+ t->Commit();
+ listener_->CommitFilesToRemove();
+ }
+
+
+ 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 3b3525dee661 -r 58f969933720 OrthancServer/ServerIndex.h
--- a/OrthancServer/ServerIndex.h Sun Dec 09 15:01:00 2012 +0100
+++ b/OrthancServer/ServerIndex.h Sun Dec 09 15:03:17 2012 +0100
@@ -62,11 +62,21 @@
std::auto_ptr listener_;
std::auto_ptr db_;
+ 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 +85,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 +137,9 @@
bool GetLastExportedResource(Json::Value& target);
+ bool IsProtectedPatient(const std::string& publicId);
+
+ void SetProtectedPatient(const std::string& publicId,
+ bool isProtected);
};
}
diff -r 3b3525dee661 -r 58f969933720 OrthancServer/main.cpp
--- a/OrthancServer/main.cpp Sun Dec 09 15:01:00 2012 +0100
+++ b/OrthancServer/main.cpp Sun Dec 09 15:03:17 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 3b3525dee661 -r 58f969933720 Resources/Configuration.json
--- a/Resources/Configuration.json Sun Dec 09 15:01:00 2012 +0100
+++ b/Resources/Configuration.json Sun Dec 09 15:03:17 2012 +0100
@@ -13,6 +13,14 @@
// Enable the transparent compression of the DICOM instances
"StorageCompression" : false,
+ // Maximum size of the storage in MB (a value of "0" indicates no
+ // limit on the storage size)
+ "MaximumStorageSize" : 0,
+
+ // Maximum number of patients that can be stored at a given time
+ // in the storage (a value of "0" indicates no limit on the number
+ // of patients)
+ "MaximumPatientCount" : 0,
/**
diff -r 3b3525dee661 -r 58f969933720 Resources/Samples/RestApi/CMakeLists.txt
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Samples/RestApi/CMakeLists.txt Sun Dec 09 15:03:17 2012 +0100
@@ -0,0 +1,47 @@
+cmake_minimum_required(VERSION 2.8)
+
+project(RestApiSample)
+
+include(ExternalProject)
+
+ExternalProject_Add(
+ ORTHANC_CORE
+ PREFIX ${CMAKE_BINARY_DIR}/Orthanc/
+ DOWNLOAD_COMMAND hg clone https://code.google.com/p/orthanc/ -r Orthanc-0.3.1
+ UPDATE_COMMAND ""
+ SOURCE_DIR ${CMAKE_BINARY_DIR}/Orthanc/src/orthanc/
+
+ # Optional step, to reuse the third-party downloads
+ PATCH_COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_SOURCE_DIR}/../../../ThirdPartyDownloads ThirdPartyDownloads
+
+ CMAKE_COMMAND ${CMAKE_COMMAND}
+ CMAKE_ARGS -DSTATIC_BUILD=ON -DSTANDALONE_BUILD=ON -DUSE_DYNAMIC_GOOGLE_LOG=OFF -DUSE_DYNAMIC_SQLITE=OFF -DONLY_CORE_LIBRARY=ON -DENABLE_SSL=OFF
+ BUILD_COMMAND $(MAKE)
+ INSTALL_COMMAND ""
+ BUILD_IN_SOURCE 0
+)
+
+ExternalProject_Get_Property(ORTHANC_CORE source_dir)
+include_directories(${source_dir})
+
+ExternalProject_Get_Property(ORTHANC_CORE binary_dir)
+link_directories(${binary_dir})
+include_directories(${binary_dir}/jsoncpp-src-0.5.0/include)
+include_directories(${binary_dir}/glog-0.3.2/src)
+
+add_executable(RestApiSample
+ Sample.cpp
+ )
+
+add_dependencies(RestApiSample ORTHANC_CORE)
+
+target_link_libraries(RestApiSample
+ # From Orthanc
+ CoreLibrary
+ GoogleLog
+ #OpenSSL
+
+ # System-wide libraries
+ pthread
+ )
+
diff -r 3b3525dee661 -r 58f969933720 Resources/Samples/RestApi/Sample.cpp
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Samples/RestApi/Sample.cpp Sun Dec 09 15:03:17 2012 +0100
@@ -0,0 +1,105 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012 Medical Physics Department, CHU of Liege,
+ * Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ **/
+
+
+#include
+#include
+#include
+#include
+#include
+
+
+/**
+ * This is a demo program that shows how to setup a REST server with
+ * the Orthanc Core API. Once the server is running, here are some
+ * sample command lines to interact with it:
+ *
+ * # curl http://localhost:8042
+ * # curl 'http://localhost:8042?name=Hide'
+ * # curl http://localhost:8042 -X DELETE
+ * # curl http://localhost:8042 -X PUT -d "PutBody"
+ * # curl http://localhost:8042 -X POST -d "PostBody"
+ **/
+
+static void GetRoot(Orthanc::RestApi::GetCall& call)
+{
+ std::string answer = "Hello world\n";
+ answer += "Glad to meet you, Mr. " + call.GetArgument("name", "Nobody") + "\n";
+ call.GetOutput().AnswerBuffer(answer, "text/plain");
+}
+
+static void DeleteRoot(Orthanc::RestApi::DeleteCall& call)
+{
+ call.GetOutput().AnswerBuffer("Hey, you have just deleted the server!\n",
+ "text/plain");
+}
+
+static void PostRoot(Orthanc::RestApi::PostCall& call)
+{
+ call.GetOutput().AnswerBuffer("I have received a POST with body: [" +
+ call.GetPostBody() + "]\n", "text/plain");
+}
+
+static void PutRoot(Orthanc::RestApi::PutCall& call)
+{
+ call.GetOutput().AnswerBuffer("I have received a PUT with body: [" +
+ call.GetPutBody() + "]\n", "text/plain");
+}
+
+int main()
+{
+ // Initialize the logging mechanism
+ google::InitGoogleLogging("Orthanc");
+ FLAGS_logtostderr = true;
+ FLAGS_minloglevel = 0; // Use the verbose mode
+ FLAGS_v = 0;
+
+ // Define the callbacks of the REST API
+ std::auto_ptr rest(new Orthanc::RestApi);
+ rest->Register("/", GetRoot);
+ rest->Register("/", PostRoot);
+ rest->Register("/", PutRoot);
+ rest->Register("/", DeleteRoot);
+
+ // Setup the embedded HTTP server
+ Orthanc::MongooseServer httpServer;
+ httpServer.SetPortNumber(8042); // Use TCP port 8042
+ httpServer.SetRemoteAccessAllowed(true); // Do not block remote requests
+ httpServer.RegisterHandler(rest.release()); // The REST API is the handler
+
+ // Start the server and wait for the user to hit "Ctrl-C"
+ httpServer.Start();
+ LOG(WARNING) << "REST server has started";
+ Orthanc::Toolbox::ServerBarrier();
+ LOG(WARNING) << "REST server has stopped";
+
+ return 0;
+}
diff -r 3b3525dee661 -r 58f969933720 UnitTests/ServerIndex.cpp
--- a/UnitTests/ServerIndex.cpp Sun Dec 09 15:01:00 2012 +0100
+++ b/UnitTests/ServerIndex.cpp Sun Dec 09 15:03:17 2012 +0100
@@ -1,6 +1,7 @@
#include "gtest/gtest.h"
#include "../OrthancServer/DatabaseWrapper.h"
+#include "../Core/Uuid.h"
#include
#include
@@ -12,7 +13,7 @@
class ServerIndexListener : public IServerIndexListener
{
public:
- std::set deletedFiles_;
+ std::vector deletedFiles_;
std::string ancestorId_;
ResourceType ancestorType_;
@@ -29,9 +30,10 @@
ancestorType_ = type;
}
- virtual void SignalFileDeleted(const std::string& fileUuid)
+ virtual void SignalFileDeleted(const FileInfo& info)
{
- deletedFiles_.insert(fileUuid);
+ const std::string fileUuid = info.GetUuid();
+ deletedFiles_.push_back(fileUuid);
LOG(INFO) << "A file must be removed: " << fileUuid;
}
};
@@ -170,8 +172,12 @@
index.DeleteResource(a[0]);
ASSERT_EQ(2u, listener.deletedFiles_.size());
- ASSERT_FALSE(listener.deletedFiles_.find("my json file") == listener.deletedFiles_.end());
- ASSERT_FALSE(listener.deletedFiles_.find("my dicom file") == listener.deletedFiles_.end());
+ ASSERT_FALSE(std::find(listener.deletedFiles_.begin(),
+ listener.deletedFiles_.end(),
+ "my json file") == listener.deletedFiles_.end());
+ ASSERT_FALSE(std::find(listener.deletedFiles_.begin(),
+ listener.deletedFiles_.end(),
+ "my dicom file") == listener.deletedFiles_.end());
ASSERT_EQ(2u, index.GetTableRecordCount("Resources"));
ASSERT_EQ(0u, index.GetTableRecordCount("Metadata"));
@@ -183,7 +189,9 @@
ASSERT_EQ(2u, index.GetTableRecordCount("GlobalProperties"));
ASSERT_EQ(3u, listener.deletedFiles_.size());
- ASSERT_FALSE(listener.deletedFiles_.find("world") == listener.deletedFiles_.end());
+ ASSERT_FALSE(std::find(listener.deletedFiles_.begin(),
+ listener.deletedFiles_.end(),
+ "world") == listener.deletedFiles_.end());
}
@@ -256,3 +264,135 @@
index.DeleteResource(a[6]);
ASSERT_EQ("", listener.ancestorId_); // No more ancestor
}
+
+
+TEST(DatabaseWrapper, PatientRecycling)
+{
+ ServerIndexListener listener;
+ DatabaseWrapper index(listener);
+
+ std::vector patients;
+ for (int i = 0; i < 10; i++)
+ {
+ std::string p = "Patient " + boost::lexical_cast(i);
+ patients.push_back(index.CreateResource(p, ResourceType_Patient));
+ index.AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10));
+ ASSERT_FALSE(index.IsProtectedPatient(patients[i]));
+ }
+
+ ASSERT_EQ(10u, index.GetTableRecordCount("Resources"));
+ ASSERT_EQ(10u, index.GetTableRecordCount("PatientRecyclingOrder"));
+
+ listener.Reset();
+
+ index.DeleteResource(patients[5]);
+ index.DeleteResource(patients[0]);
+ ASSERT_EQ(8u, index.GetTableRecordCount("Resources"));
+ ASSERT_EQ(8u, index.GetTableRecordCount("PatientRecyclingOrder"));
+
+ ASSERT_EQ(2u, listener.deletedFiles_.size());
+ ASSERT_EQ("Patient 5", listener.deletedFiles_[0]);
+ ASSERT_EQ("Patient 0", listener.deletedFiles_[1]);
+
+ int64_t p;
+ ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[1]);
+ index.DeleteResource(p);
+ ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]);
+ index.DeleteResource(p);
+ ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[3]);
+ index.DeleteResource(p);
+ ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]);
+ index.DeleteResource(p);
+ ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[6]);
+ index.DeleteResource(p);
+ index.DeleteResource(patients[8]);
+ ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[7]);
+ index.DeleteResource(p);
+ ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[9]);
+ index.DeleteResource(p);
+ ASSERT_FALSE(index.SelectPatientToRecycle(p));
+
+ ASSERT_EQ(10u, listener.deletedFiles_.size());
+ ASSERT_EQ(0u, index.GetTableRecordCount("Resources"));
+ ASSERT_EQ(0u, index.GetTableRecordCount("PatientRecyclingOrder"));
+}
+
+
+TEST(DatabaseWrapper, PatientProtection)
+{
+ ServerIndexListener listener;
+ DatabaseWrapper index(listener);
+
+ std::vector patients;
+ for (int i = 0; i < 5; i++)
+ {
+ std::string p = "Patient " + boost::lexical_cast(i);
+ patients.push_back(index.CreateResource(p, ResourceType_Patient));
+ index.AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10));
+ ASSERT_FALSE(index.IsProtectedPatient(patients[i]));
+ }
+
+ ASSERT_EQ(5u, index.GetTableRecordCount("Resources"));
+ ASSERT_EQ(5u, index.GetTableRecordCount("PatientRecyclingOrder"));
+
+ ASSERT_FALSE(index.IsProtectedPatient(patients[2]));
+ index.SetProtectedPatient(patients[2], true);
+ ASSERT_TRUE(index.IsProtectedPatient(patients[2]));
+ ASSERT_EQ(4u, index.GetTableRecordCount("PatientRecyclingOrder"));
+ ASSERT_EQ(5u, index.GetTableRecordCount("Resources"));
+
+ index.SetProtectedPatient(patients[2], true);
+ ASSERT_TRUE(index.IsProtectedPatient(patients[2]));
+ ASSERT_EQ(4u, index.GetTableRecordCount("PatientRecyclingOrder"));
+ index.SetProtectedPatient(patients[2], false);
+ ASSERT_FALSE(index.IsProtectedPatient(patients[2]));
+ ASSERT_EQ(5u, index.GetTableRecordCount("PatientRecyclingOrder"));
+ index.SetProtectedPatient(patients[2], false);
+ ASSERT_FALSE(index.IsProtectedPatient(patients[2]));
+ ASSERT_EQ(5u, index.GetTableRecordCount("PatientRecyclingOrder"));
+
+ ASSERT_EQ(5u, index.GetTableRecordCount("Resources"));
+ ASSERT_EQ(5u, index.GetTableRecordCount("PatientRecyclingOrder"));
+ index.SetProtectedPatient(patients[2], true);
+ ASSERT_TRUE(index.IsProtectedPatient(patients[2]));
+ ASSERT_EQ(4u, index.GetTableRecordCount("PatientRecyclingOrder"));
+ index.SetProtectedPatient(patients[2], false);
+ ASSERT_FALSE(index.IsProtectedPatient(patients[2]));
+ ASSERT_EQ(5u, index.GetTableRecordCount("PatientRecyclingOrder"));
+ index.SetProtectedPatient(patients[3], true);
+ ASSERT_TRUE(index.IsProtectedPatient(patients[3]));
+ ASSERT_EQ(4u, index.GetTableRecordCount("PatientRecyclingOrder"));
+
+ ASSERT_EQ(5u, index.GetTableRecordCount("Resources"));
+ ASSERT_EQ(0u, listener.deletedFiles_.size());
+
+ // Unprotecting a patient puts it at the last position in the recycling queue
+ int64_t p;
+ ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[0]);
+ index.DeleteResource(p);
+ ASSERT_TRUE(index.SelectPatientToRecycle(p, patients[1])); ASSERT_EQ(p, patients[4]);
+ ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[1]);
+ index.DeleteResource(p);
+ ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]);
+ index.DeleteResource(p);
+ ASSERT_FALSE(index.SelectPatientToRecycle(p, patients[2]));
+ ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]);
+ index.DeleteResource(p);
+ // "patients[3]" is still protected
+ ASSERT_FALSE(index.SelectPatientToRecycle(p));
+
+ ASSERT_EQ(4u, listener.deletedFiles_.size());
+ ASSERT_EQ(1u, index.GetTableRecordCount("Resources"));
+ ASSERT_EQ(0u, index.GetTableRecordCount("PatientRecyclingOrder"));
+
+ index.SetProtectedPatient(patients[3], false);
+ ASSERT_EQ(1u, index.GetTableRecordCount("PatientRecyclingOrder"));
+ ASSERT_FALSE(index.SelectPatientToRecycle(p, patients[3]));
+ ASSERT_TRUE(index.SelectPatientToRecycle(p, patients[2]));
+ ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[3]);
+ index.DeleteResource(p);
+
+ ASSERT_EQ(5u, listener.deletedFiles_.size());
+ ASSERT_EQ(0u, index.GetTableRecordCount("Resources"));
+ ASSERT_EQ(0u, index.GetTableRecordCount("PatientRecyclingOrder"));
+}