changeset 277:58f969933720

merge with Orthanc-0.3.1
author Sebastien Jodogne <s.jodogne@gmail.com>
date Sun, 09 Dec 2012 15:03:17 +0100
parents 8af8754a7a8e (diff) 3b3525dee661 (current diff)
children 771f12042be9
files CMakeLists.txt
diffstat 23 files changed, 828 insertions(+), 101 deletions(-) [+]
line wrap: on
line diff
--- 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
--- 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
--- 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 "???";
--- 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);
--- 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;
--- 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)
--- 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
--- 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 @@
               <ul data-role="listview" data-inset="true" data-theme="a"  id="patient-info">
               </ul>
               <p>
-                <a href="#find-patients" data-role="button" data-icon="search">Go to patient finder</a>
+                <div class="switch-container">
+                  <select name="protection" id="protection" data-role="slider">
+	            <option value="off">Unprotected</option>
+	            <option value="on">Protected</option>
+                  </select>
+                </div>
+                <!--a href="#find-patients" data-role="button" data-icon="search">Go to patient finder</a-->
                 <a href="#" data-role="button" data-icon="delete" id="patient-delete">Delete this patient</a>
                 <a href="#" data-role="button" data-icon="gear" id="patient-archive">Download ZIP</a>
               </p>
--- 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
+  });
+});
--- 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 <glog/logging.h>
@@ -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<FileContentType>(context.GetIntValue(1)),
+                      static_cast<uint64_t>(context.GetInt64Value(2)),
+                      static_cast<CompressionType>(context.GetIntValue(3)),
+                      static_cast<uint64_t>(context.GetInt64Value(4)));
+        
+        listener_.SignalFileDeleted(info);
       }
     };
 
@@ -743,9 +750,9 @@
       LOG(INFO) << "Version of the Orthanc database: " << version;
       unsigned int v = boost::lexical_cast<unsigned int>(version);
 
-      // This version of Orthanc is only compatible with version 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
+    }
+  }
 }
--- 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);
 
--- 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;
   };
 }
--- 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 <string>
 #include <set>
 #include <json/json.h>
+#include <stdint.h>
 #include "../Core/HttpServer/MongooseServer.h"
 
 namespace Orthanc
--- 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<ResourceType_Study>);
     Register("/series/{id}/archive", GetArchive<ResourceType_Series>);
 
+    Register("/patients/{id}/protected", IsProtectedPatient);
+    Register("/patients/{id}/protected", SetPatientProtection);
     Register("/instances/{id}/file", GetInstanceFile);
     Register("/instances/{id}/tags", GetInstanceTags<false>);
     Register("/instances/{id}/simplified-tags", GetInstanceTags<true>);
--- 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");
--- 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)
--- 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<std::string> pendingFilesToRemove_;
 
     public:
       ServerIndexListener(ServerContext& context) : 
@@ -73,6 +74,17 @@
       void Reset()
       {
         hasRemainingLevel_ = false;
+        pendingFilesToRemove_.clear();
+      }
+
+      void CommitFilesToRemove()
+      {
+        for (std::list<std::string>::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<SQLite::Transaction> 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<unsigned int>(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<unsigned int>(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<SQLite::Transaction> 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";
+  }
+
 }
--- 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<Internals::ServerIndexListener> listener_;
     std::auto_ptr<DatabaseWrapper> 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<FileInfo> 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);
   };
 }
--- 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);
 
     {
--- 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,
 
 
     /**
--- /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 
+  )
+
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include <Core/HttpServer/MongooseServer.h>
+#include <Core/RestApi/RestApi.h>
+#include <Core/Toolbox.h>
+#include <glog/logging.h>
+#include <stdio.h>
+
+
+/**
+ * This is a demo program that shows how to setup a REST server with
+ * the Orthanc Core API. Once the server is running, here are some 
+ * sample command lines to interact with it:
+ * 
+ *  # curl http://localhost:8042
+ *  # curl 'http://localhost:8042?name=Hide'
+ *  # curl http://localhost:8042 -X DELETE
+ *  # curl http://localhost:8042 -X PUT -d "PutBody"
+ *  # curl http://localhost:8042 -X POST -d "PostBody"
+ **/
+
+static void GetRoot(Orthanc::RestApi::GetCall& call)
+{
+  std::string answer = "Hello world\n";
+  answer += "Glad to meet you, Mr. " + call.GetArgument("name", "Nobody") + "\n";
+  call.GetOutput().AnswerBuffer(answer, "text/plain");
+}
+ 
+static void DeleteRoot(Orthanc::RestApi::DeleteCall& call)
+{
+  call.GetOutput().AnswerBuffer("Hey, you have just deleted the server!\n",
+                                "text/plain");
+}
+ 
+static void PostRoot(Orthanc::RestApi::PostCall& call)
+{
+  call.GetOutput().AnswerBuffer("I have received a POST with body: [" +
+                                call.GetPostBody() + "]\n", "text/plain");
+}
+ 
+static void PutRoot(Orthanc::RestApi::PutCall& call)
+{
+  call.GetOutput().AnswerBuffer("I have received a PUT with body: [" +
+                                call.GetPutBody() + "]\n", "text/plain");
+}
+ 
+int main()
+{
+  // Initialize the logging mechanism
+  google::InitGoogleLogging("Orthanc");
+  FLAGS_logtostderr = true;
+  FLAGS_minloglevel = 0;                      // Use the verbose mode
+  FLAGS_v = 0;
+  
+  // Define the callbacks of the REST API
+  std::auto_ptr<Orthanc::RestApi> rest(new Orthanc::RestApi);
+  rest->Register("/", GetRoot);
+  rest->Register("/", PostRoot);
+  rest->Register("/", PutRoot);
+  rest->Register("/", DeleteRoot);
+
+  // Setup the embedded HTTP server
+  Orthanc::MongooseServer httpServer;
+  httpServer.SetPortNumber(8042);             // Use TCP port 8042
+  httpServer.SetRemoteAccessAllowed(true);    // Do not block remote requests
+  httpServer.RegisterHandler(rest.release()); // The REST API is the handler
+
+  // Start the server and wait for the user to hit "Ctrl-C"
+  httpServer.Start();
+  LOG(WARNING) << "REST server has started";
+  Orthanc::Toolbox::ServerBarrier();
+  LOG(WARNING) << "REST server has stopped";
+
+  return 0;
+}
--- 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 <ctype.h>
 #include <glog/logging.h>
@@ -12,7 +13,7 @@
   class ServerIndexListener : public IServerIndexListener
   {
   public:
-    std::set<std::string> deletedFiles_;
+    std::vector<std::string> 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<int64_t> patients;
+  for (int i = 0; i < 10; i++)
+  {
+    std::string p = "Patient " + boost::lexical_cast<std::string>(i);
+    patients.push_back(index.CreateResource(p, ResourceType_Patient));
+    index.AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10));
+    ASSERT_FALSE(index.IsProtectedPatient(patients[i]));
+  }
+
+  ASSERT_EQ(10u, index.GetTableRecordCount("Resources")); 
+  ASSERT_EQ(10u, index.GetTableRecordCount("PatientRecyclingOrder")); 
+
+  listener.Reset();
+
+  index.DeleteResource(patients[5]);
+  index.DeleteResource(patients[0]);
+  ASSERT_EQ(8u, index.GetTableRecordCount("Resources")); 
+  ASSERT_EQ(8u, index.GetTableRecordCount("PatientRecyclingOrder"));
+
+  ASSERT_EQ(2u, listener.deletedFiles_.size());
+  ASSERT_EQ("Patient 5", listener.deletedFiles_[0]);
+  ASSERT_EQ("Patient 0", listener.deletedFiles_[1]);
+
+  int64_t p;
+  ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[1]);
+  index.DeleteResource(p);
+  ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]);
+  index.DeleteResource(p);
+  ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[3]);
+  index.DeleteResource(p);
+  ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]);
+  index.DeleteResource(p);
+  ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[6]);
+  index.DeleteResource(p);
+  index.DeleteResource(patients[8]);
+  ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[7]);
+  index.DeleteResource(p);
+  ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[9]);
+  index.DeleteResource(p);
+  ASSERT_FALSE(index.SelectPatientToRecycle(p));
+
+  ASSERT_EQ(10u, listener.deletedFiles_.size());
+  ASSERT_EQ(0u, index.GetTableRecordCount("Resources")); 
+  ASSERT_EQ(0u, index.GetTableRecordCount("PatientRecyclingOrder")); 
+}
+
+
+TEST(DatabaseWrapper, PatientProtection)
+{
+  ServerIndexListener listener;
+  DatabaseWrapper index(listener);
+
+  std::vector<int64_t> patients;
+  for (int i = 0; i < 5; i++)
+  {
+    std::string p = "Patient " + boost::lexical_cast<std::string>(i);
+    patients.push_back(index.CreateResource(p, ResourceType_Patient));
+    index.AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10));
+    ASSERT_FALSE(index.IsProtectedPatient(patients[i]));
+  }
+
+  ASSERT_EQ(5u, index.GetTableRecordCount("Resources")); 
+  ASSERT_EQ(5u, index.GetTableRecordCount("PatientRecyclingOrder")); 
+
+  ASSERT_FALSE(index.IsProtectedPatient(patients[2]));
+  index.SetProtectedPatient(patients[2], true);
+  ASSERT_TRUE(index.IsProtectedPatient(patients[2]));
+  ASSERT_EQ(4u, index.GetTableRecordCount("PatientRecyclingOrder"));
+  ASSERT_EQ(5u, index.GetTableRecordCount("Resources")); 
+
+  index.SetProtectedPatient(patients[2], true);
+  ASSERT_TRUE(index.IsProtectedPatient(patients[2]));
+  ASSERT_EQ(4u, index.GetTableRecordCount("PatientRecyclingOrder")); 
+  index.SetProtectedPatient(patients[2], false);
+  ASSERT_FALSE(index.IsProtectedPatient(patients[2]));
+  ASSERT_EQ(5u, index.GetTableRecordCount("PatientRecyclingOrder")); 
+  index.SetProtectedPatient(patients[2], false);
+  ASSERT_FALSE(index.IsProtectedPatient(patients[2]));
+  ASSERT_EQ(5u, index.GetTableRecordCount("PatientRecyclingOrder")); 
+
+  ASSERT_EQ(5u, index.GetTableRecordCount("Resources")); 
+  ASSERT_EQ(5u, index.GetTableRecordCount("PatientRecyclingOrder")); 
+  index.SetProtectedPatient(patients[2], true);
+  ASSERT_TRUE(index.IsProtectedPatient(patients[2]));
+  ASSERT_EQ(4u, index.GetTableRecordCount("PatientRecyclingOrder"));
+  index.SetProtectedPatient(patients[2], false);
+  ASSERT_FALSE(index.IsProtectedPatient(patients[2]));
+  ASSERT_EQ(5u, index.GetTableRecordCount("PatientRecyclingOrder")); 
+  index.SetProtectedPatient(patients[3], true);
+  ASSERT_TRUE(index.IsProtectedPatient(patients[3]));
+  ASSERT_EQ(4u, index.GetTableRecordCount("PatientRecyclingOrder")); 
+
+  ASSERT_EQ(5u, index.GetTableRecordCount("Resources")); 
+  ASSERT_EQ(0u, listener.deletedFiles_.size());
+
+  // Unprotecting a patient puts it at the last position in the recycling queue
+  int64_t p;
+  ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[0]);
+  index.DeleteResource(p);
+  ASSERT_TRUE(index.SelectPatientToRecycle(p, patients[1])); ASSERT_EQ(p, patients[4]);
+  ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[1]);
+  index.DeleteResource(p);
+  ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]);
+  index.DeleteResource(p);
+  ASSERT_FALSE(index.SelectPatientToRecycle(p, patients[2]));
+  ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]);
+  index.DeleteResource(p);
+  // "patients[3]" is still protected
+  ASSERT_FALSE(index.SelectPatientToRecycle(p));
+
+  ASSERT_EQ(4u, listener.deletedFiles_.size());
+  ASSERT_EQ(1u, index.GetTableRecordCount("Resources")); 
+  ASSERT_EQ(0u, index.GetTableRecordCount("PatientRecyclingOrder")); 
+
+  index.SetProtectedPatient(patients[3], false);
+  ASSERT_EQ(1u, index.GetTableRecordCount("PatientRecyclingOrder")); 
+  ASSERT_FALSE(index.SelectPatientToRecycle(p, patients[3]));
+  ASSERT_TRUE(index.SelectPatientToRecycle(p, patients[2]));
+  ASSERT_TRUE(index.SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[3]);
+  index.DeleteResource(p);
+
+  ASSERT_EQ(5u, listener.deletedFiles_.size());
+  ASSERT_EQ(0u, index.GetTableRecordCount("Resources")); 
+  ASSERT_EQ(0u, index.GetTableRecordCount("PatientRecyclingOrder")); 
+}