view OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp @ 5876:7f4fab033c87 get-scu

RejectedSopClasses + /queries/../get to retrieve a C-FIND answer
author Alain Mazy <am@orthanc.team>
date Thu, 21 Nov 2024 12:17:47 +0100
parents f7adfb22e20e
children 3ddd1b0231e9
line wrap: on
line source

/**
 * Orthanc - A Lightweight, RESTful DICOM Store
 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 * Department, University Hospital of Liege, Belgium
 * Copyright (C) 2017-2023 Osimis S.A., Belgium
 * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
 * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 **/


#define HOUSEKEEPER_NAME "housekeeper"

#include "../Common/OrthancPluginCppWrapper.h"
#include "../../../../OrthancFramework/Sources/Compatibility.h"

#include <boost/thread.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <boost/date_time/special_defs.hpp>
#include <json/value.h>
#include <json/writer.h>
#include <string.h>
#include <iostream>
#include <algorithm>
#include <map>
#include <list>
#include <time.h>

static int globalPropertyId_ = 0;
static bool force_ = false;
static unsigned int throttleDelay_ = 0;
static std::unique_ptr<boost::thread> workerThread_;
static bool workerThreadShouldStop_ = false;
static bool triggerOnStorageCompressionChange_ = true;
static bool triggerOnMainDicomTagsChange_ = true;
static bool triggerOnUnnecessaryDicomAsJsonFiles_ = true;
static bool triggerOnIngestTranscodingChange_ = true;
static bool triggerOnDicomWebCacheChange_ = true;
static std::string limitMainDicomTagsReconstructLevel_ = "";
static std::string limitToChange_ = "";
static std::string limitToUrl_ = "";


struct RunningPeriod
{
  int fromHour_;
  int toHour_;
  int weekday_;

  RunningPeriod(const std::string& weekday, const std::string& period)
  {
    if (weekday == "Monday")
    {
      weekday_ = 1;
    }
    else if (weekday == "Tuesday")
    {
      weekday_ = 2;
    }
    else if (weekday == "Wednesday")
    {
      weekday_ = 3;
    }
    else if (weekday == "Thursday")
    {
      weekday_ = 4;
    }
    else if (weekday == "Friday")
    {
      weekday_ = 5;
    }
    else if (weekday == "Saturday")
    {
      weekday_ = 6;
    }
    else if (weekday == "Sunday")
    {
      weekday_ = 0;
    }
    else
    {
      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: invalid schedule: unknown 'day': " + weekday);
      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
    }

    std::vector<std::string> hours;
    boost::split(hours, period, boost::is_any_of("-"));

    fromHour_ = boost::lexical_cast<int>(hours[0]);
    toHour_ = boost::lexical_cast<int>(hours[1]);
  }

  bool isInPeriod() const
  {
    time_t now = time(NULL);
    tm* nowLocalTime = localtime(&now);

    if (nowLocalTime->tm_wday != weekday_)
    {
      return false;
    }

    if (nowLocalTime->tm_hour >= fromHour_ && nowLocalTime->tm_hour < toHour_)
    {
      return true;
    }

    return false;
  }
};


struct RunningPeriods
{
  std::list<RunningPeriod> runningPeriods_;

  void load(const Json::Value& scheduleConfiguration)
  {
//   "Monday": ["0-6", "20-24"],

    Json::Value::Members names = scheduleConfiguration.getMemberNames();

    for (Json::Value::Members::const_iterator it = names.begin();
      it != names.end(); ++it)
    {
      for (Json::Value::ArrayIndex i = 0; i < scheduleConfiguration[*it].size(); i++)
      {
        runningPeriods_.push_back(RunningPeriod(*it, scheduleConfiguration[*it][i].asString()));
      }
    }
  }

  bool isInPeriod()
  {
    if (runningPeriods_.size() == 0)
    {
      return true;  // if no config: always run
    }

    for (std::list<RunningPeriod>::const_iterator it = runningPeriods_.begin();
      it != runningPeriods_.end(); ++it)
    {
      if (it->isInPeriod())
      {
        return true;
      }
    }
    return false;
  }
};

RunningPeriods runningPeriods_;


struct DbConfiguration
{
  std::string orthancVersion;
  std::string patientsMainDicomTagsSignature;
  std::string studiesMainDicomTagsSignature;
  std::string seriesMainDicomTagsSignature;
  std::string instancesMainDicomTagsSignature;
  std::string ingestTranscoding;
  std::string dicomWebVersion;
  bool storageCompressionEnabled;

  DbConfiguration()
  : storageCompressionEnabled(false)
  {
  }

  bool IsDefined() const
  {
    return !orthancVersion.empty();
  }

  void Clear()
  {
    orthancVersion.clear();
    patientsMainDicomTagsSignature.clear();
    studiesMainDicomTagsSignature.clear();
    seriesMainDicomTagsSignature.clear();
    instancesMainDicomTagsSignature.clear();
    ingestTranscoding.clear();
    dicomWebVersion.clear();
  }

  void ToJson(Json::Value& target)
  {
    if (!IsDefined())
    {
      target = Json::nullValue;
    }
    else
    {
      Json::Value signatures;

      target = Json::objectValue;

      // default main dicom tags signature are the one from Orthanc 1.4.2 (last time the list was changed):
      signatures["Patient"] = patientsMainDicomTagsSignature;
      signatures["Study"] = studiesMainDicomTagsSignature;
      signatures["Series"] = seriesMainDicomTagsSignature;
      signatures["Instance"] = instancesMainDicomTagsSignature;

      target["MainDicomTagsSignature"] = signatures;
      target["OrthancVersion"] = orthancVersion;
      target["StorageCompressionEnabled"] = storageCompressionEnabled;
      target["IngestTranscoding"] = ingestTranscoding;
      target["DicomWebVersion"] = dicomWebVersion;
    }
  }

  void FromJson(Json::Value& source)
  {
    if (!source.isNull())
    {
      orthancVersion = source["OrthancVersion"].asString();
      if (source.isMember("DicomWebVersion"))
      {
        dicomWebVersion = source["DicomWebVersion"].asString();
      }
      else
      {
        dicomWebVersion = "1.14"; // the first change that requires processing has been introduced between 1.14 & 1.15
      }

      const Json::Value& signatures = source["MainDicomTagsSignature"];
      patientsMainDicomTagsSignature = signatures["Patient"].asString();
      studiesMainDicomTagsSignature = signatures["Study"].asString();
      seriesMainDicomTagsSignature = signatures["Series"].asString();
      instancesMainDicomTagsSignature = signatures["Instance"].asString();

      storageCompressionEnabled = source["StorageCompressionEnabled"].asBool();
      ingestTranscoding = source["IngestTranscoding"].asString();
    }
  }
};


struct PluginStatus
{
  int statusVersion;
  int64_t lastProcessedChange;
  int64_t lastChangeToProcess;
  boost::posix_time::ptime lastTimeStarted;

  DbConfiguration currentlyProcessingConfiguration; // last configuration being processed (has not reached last change yet)
  DbConfiguration lastProcessedConfiguration;       // last configuration that has been fully processed (till last change)

  PluginStatus()
  : statusVersion(1),
    lastProcessedChange(-1),
    lastChangeToProcess(-1),
    lastTimeStarted(boost::date_time::not_a_date_time)
  {
  }

  void ToJson(Json::Value& target)
  {
    target = Json::objectValue;

    target["Version"] = statusVersion;
    target["LastProcessedChange"] = Json::Value::Int64(lastProcessedChange);
    target["LastChangeToProcess"] = Json::Value::Int64(lastChangeToProcess);
    
    if (lastTimeStarted == boost::date_time::not_a_date_time)
    {
      target["LastTimeStarted"] = Json::Value::null;  
    }
    else
    {
      target["LastTimeStarted"] = boost::posix_time::to_iso_string(lastTimeStarted);
    }

    currentlyProcessingConfiguration.ToJson(target["CurrentlyProcessingConfiguration"]);
    lastProcessedConfiguration.ToJson(target["LastProcessedConfiguration"]);
  }

  void FromJson(Json::Value& source)
  {
    statusVersion = source["Version"].asInt();
    lastProcessedChange = source["LastProcessedChange"].asInt64();
    lastChangeToProcess = source["LastChangeToProcess"].asInt64();
    if (source["LastTimeStarted"].isNull())
    {
      lastTimeStarted = boost::date_time::not_a_date_time;
    }
    else
    {
      lastTimeStarted = boost::posix_time::from_iso_string(source["LastTimeStarted"].asString());
    }

    Json::Value& current = source["CurrentlyProcessingConfiguration"];
    Json::Value& last = source["LastProcessedConfiguration"];

    currentlyProcessingConfiguration.FromJson(current);
    lastProcessedConfiguration.FromJson(last);
  }
};

static PluginStatus pluginStatus_;
static boost::recursive_mutex pluginStatusMutex_;

static void ReadStatusFromDb()
{
  boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);

  OrthancPlugins::OrthancString globalPropertyContent;

  globalPropertyContent.Assign(OrthancPluginGetGlobalProperty(OrthancPlugins::GetGlobalContext(),
                                                              globalPropertyId_,
                                                              ""));

  if (!globalPropertyContent.IsNullOrEmpty())
  {
    Json::Value jsonStatus;
    globalPropertyContent.ToJson(jsonStatus);
    pluginStatus_.FromJson(jsonStatus);
  }
  else
  {
    // default config
    pluginStatus_.statusVersion = 1;
    pluginStatus_.lastProcessedChange = -1;
    pluginStatus_.lastChangeToProcess = -1;
    pluginStatus_.lastTimeStarted = boost::date_time::not_a_date_time;
    
    pluginStatus_.lastProcessedConfiguration.orthancVersion = "1.9.0"; // when we don't know, we assume some files were stored with Orthanc 1.9.0 (last version saving the dicom-as-json files)
    pluginStatus_.lastProcessedConfiguration.dicomWebVersion = "1.14"; // the first change that requires processing has been introduced between 1.14 & 1.15

    // default main dicom tags signature are the one from Orthanc 1.4.2 (last time the list was changed):
    pluginStatus_.lastProcessedConfiguration.patientsMainDicomTagsSignature = "0010,0010;0010,0020;0010,0030;0010,0040;0010,1000";
    pluginStatus_.lastProcessedConfiguration.studiesMainDicomTagsSignature = "0008,0020;0008,0030;0008,0050;0008,0080;0008,0090;0008,1030;0020,000d;0020,0010;0032,1032;0032,1060";
    pluginStatus_.lastProcessedConfiguration.seriesMainDicomTagsSignature = "0008,0021;0008,0031;0008,0060;0008,0070;0008,1010;0008,103e;0008,1070;0018,0010;0018,0015;0018,0024;0018,1030;0018,1090;0018,1400;0020,000e;0020,0011;0020,0037;0020,0105;0020,1002;0040,0254;0054,0081;0054,0101;0054,1000";
    pluginStatus_.lastProcessedConfiguration.instancesMainDicomTagsSignature = "0008,0012;0008,0013;0008,0018;0020,0012;0020,0013;0020,0032;0020,0037;0020,0100;0020,4000;0028,0008;0054,1330"; 
  }
}

static void SaveStatusInDb()
{
  boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);

  Json::Value jsonStatus;
  pluginStatus_.ToJson(jsonStatus);

  Json::StreamWriterBuilder builder;
  builder.settings_["indentation"] = "   ";
  std::string serializedStatus = Json::writeString(builder, jsonStatus);

  OrthancPluginSetGlobalProperty(OrthancPlugins::GetGlobalContext(),
                                 globalPropertyId_,
                                 serializedStatus.c_str());
}

static void GetCurrentDbConfiguration(DbConfiguration& configuration)
{
  Json::Value signatures;
  Json::Value systemInfo;

  OrthancPlugins::RestApiGet(systemInfo, "/system", false);
  configuration.patientsMainDicomTagsSignature = systemInfo["MainDicomTags"]["Patient"].asString();
  configuration.studiesMainDicomTagsSignature = systemInfo["MainDicomTags"]["Study"].asString();
  configuration.seriesMainDicomTagsSignature = systemInfo["MainDicomTags"]["Series"].asString();
  configuration.instancesMainDicomTagsSignature = systemInfo["MainDicomTags"]["Instance"].asString();
  configuration.storageCompressionEnabled = systemInfo["StorageCompression"].asBool();
  configuration.ingestTranscoding = systemInfo["IngestTranscoding"].asString();

  configuration.orthancVersion = OrthancPlugins::GetGlobalContext()->orthancVersion;

  Json::Value pluginInfo;
  if (OrthancPlugins::RestApiGet(pluginInfo, "/plugins/dicom-web", false))
  {
    configuration.dicomWebVersion = pluginInfo["Version"].asString();
  }
}

static void CheckNeedsProcessing(bool& needsReconstruct, bool& needsReingest, bool& needsDicomWebCaching, const DbConfiguration& current, const DbConfiguration& last)
{
  needsReconstruct = false;
  needsReingest = false;
  needsDicomWebCaching = false;

  if (!last.IsDefined())
  {
    return;
  }

  const char* lastVersion = last.orthancVersion.c_str();

  if (!OrthancPlugins::CheckMinimalVersion(lastVersion, 1, 9, 1))
  {
    if (triggerOnUnnecessaryDicomAsJsonFiles_)
    {
      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: your storage might still contain some dicom-as-json files -> will perform housekeeping");
      needsReconstruct = true;  // the default reconstruct removes the dicom-as-json
    }
    else
    {
      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: your storage might still contain some dicom-as-json files but the trigger has been disabled");
    }
  }

  if (last.patientsMainDicomTagsSignature != current.patientsMainDicomTagsSignature)
  {
    if (triggerOnMainDicomTagsChange_)
    {
      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: Patient main dicom tags have changed, -> will perform housekeeping");
      needsReconstruct = true;
    }
    else
    {
      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: Patient main dicom tags have changed but the trigger is disabled");
    }
  }

  if (last.studiesMainDicomTagsSignature != current.studiesMainDicomTagsSignature)
  {
    if (triggerOnMainDicomTagsChange_)
    {
      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: Study main dicom tags have changed, -> will perform housekeeping");
      needsReconstruct = true;
    }
    else
    {
      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: Study main dicom tags have changed but the trigger is disabled");
    }
  }

  if (last.seriesMainDicomTagsSignature != current.seriesMainDicomTagsSignature)
  {
    if (triggerOnMainDicomTagsChange_)
    {
      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: Series main dicom tags have changed, -> will perform housekeeping");
      needsReconstruct = true;
    }
    else
    {
      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: Series main dicom tags have changed but the trigger is disabled");
    }
  }

  if (last.instancesMainDicomTagsSignature != current.instancesMainDicomTagsSignature)
  {
    if (triggerOnMainDicomTagsChange_)
    {
      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: Instance main dicom tags have changed, -> will perform housekeeping");
      needsReconstruct = true;
    }
    else
    {
      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: Instance main dicom tags have changed but the trigger is disabled");
    }
  }

  if (current.storageCompressionEnabled != last.storageCompressionEnabled)
  {
    if (triggerOnStorageCompressionChange_)
    {
      if (current.storageCompressionEnabled)
      {
        ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: storage compression is now enabled -> will perform housekeeping");
      }
      else
      {
        ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: storage compression is now disabled -> will perform housekeeping");
      }
      
      needsReingest = true;
    }
    else
    {
      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: storage compression has changed but the trigger is disabled");
    }
  }

  if (current.ingestTranscoding != last.ingestTranscoding)
  {
    if (triggerOnIngestTranscodingChange_)
    {
      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: ingest transcoding has changed -> will perform housekeeping");
      
      needsReingest = true;
    }
    else
    {
      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: ingest transcoding has changed but the trigger is disabled");
    }
  }

  if (!current.dicomWebVersion.empty())
  {
    if (last.dicomWebVersion.empty())
    {
      if (triggerOnDicomWebCacheChange_)
      {
        ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: DicomWEB plugin is enabled and the housekeeper has never run, you might miss series metadata cache -> will perform housekeeping");
      }
      needsDicomWebCaching = triggerOnDicomWebCacheChange_;
    }
    else
    {
      const char* lastDicomWebVersion = last.dicomWebVersion.c_str();

      if (!OrthancPlugins::CheckMinimalVersion(lastDicomWebVersion, 1, 15, 0))
      {
        if (triggerOnDicomWebCacheChange_)
        {
          ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: DicomWEB plugin might miss series metadata cache -> will perform housekeeping");
          needsDicomWebCaching = true;
        }
        else
        {
          ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: DicomWEB plugin might miss series metadata cache but the trigger has been disabled");
        }
      }
    }
  }
}

static bool ProcessChanges(bool needsReconstruct, bool needsReingest, bool needsDicomWebCaching, const DbConfiguration& currentDbConfiguration)
{
  Json::Value changes;

  {
    boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);

    pluginStatus_.currentlyProcessingConfiguration = currentDbConfiguration;

    OrthancPlugins::RestApiGet(changes, "/changes?since=" + boost::lexical_cast<std::string>(pluginStatus_.lastProcessedChange) + "&limit=100", false);
  }

  if (changes["Changes"].size() > 0)
  {
    for (Json::ArrayIndex i = 0; i < changes["Changes"].size(); i++)
    {
      const Json::Value& change = changes["Changes"][i];
      int64_t seq = change["Seq"].asInt64();

      if (!limitToChange_.empty()) // if updating only maindicomtags for a single level 
      {
        if (change["ChangeType"] == limitToChange_)
        {
          Json::Value result;
          Json::Value request;
          request["ReconstructFiles"] = false;
          request["LimitToThisLevelMainDicomTags"] = true;
          OrthancPlugins::RestApiPost(result, "/" + limitToUrl_ + "/" + change["ID"].asString() + "/reconstruct", request, false);
        }
      }
      else
      {
        if (change["ChangeType"] == "NewStudy") // some StableStudy might be missing if orthanc was shutdown during a StableAge -> consider only the NewStudy events that can not be missed
        {
          Json::Value result;

          if (needsReconstruct || needsReingest)
          {
            Json::Value request;
            if (needsReingest)
            {
              request["ReconstructFiles"] = true;
            }
            OrthancPlugins::RestApiPost(result, "/studies/" + change["ID"].asString() + "/reconstruct", request, false);
          }

          if (needsDicomWebCaching)
          {
            Json::Value request;
            OrthancPlugins::RestApiPost(result, "/studies/" + change["ID"].asString() + "/update-dicomweb-cache", request, true);
          }
        }
      }

      {
        boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);

        pluginStatus_.lastProcessedChange = seq;

        if (seq >= pluginStatus_.lastChangeToProcess)  // we are done !
        {
          return true;
        }
      }

      if (change["ChangeType"] == "NewStudy")
      {
        boost::this_thread::sleep(boost::posix_time::milliseconds(throttleDelay_ * 1000));
      }
    }
  }
  else
  {
    // if the change list is empty and Done is true, it means that there is nothing to process anymore
    if (changes["Done"].asBool())
    {
      boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);

      pluginStatus_.lastProcessedChange = changes["Last"].asInt64();

      return true;
    }
  }

  return false;
}


static void WorkerThread()
{
  OrthancPluginSetCurrentThreadName(OrthancPlugins::GetGlobalContext(), "HOUSEKEEPER");

  DbConfiguration currentDbConfiguration;

  OrthancPluginLogWarning(OrthancPlugins::GetGlobalContext(), "Starting Housekeeper worker thread");

  ReadStatusFromDb();

  GetCurrentDbConfiguration(currentDbConfiguration);

  bool needsReconstruct = false;
  bool needsReingest = false;
  bool needsFullProcessing = false;
  bool needsProcessing = false;
  bool needsDicomWebCaching = false;

  {
    boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);

    // compare with last full processed configuration
    CheckNeedsProcessing(needsReconstruct, needsReingest, needsDicomWebCaching, currentDbConfiguration, pluginStatus_.lastProcessedConfiguration);
    needsFullProcessing = needsReconstruct || needsReingest || needsDicomWebCaching;
    needsProcessing = needsFullProcessing;

      // if a processing was in progress, check if the config has changed since
    if (pluginStatus_.currentlyProcessingConfiguration.IsDefined())
    {
      needsProcessing = true;       // since a processing was in progress, we need at least a partial processing

      bool needsReconstruct2 = false;
      bool needsReingest2 = false;
      bool needsDicomWebCaching2 = false;

      CheckNeedsProcessing(needsReconstruct2, needsReingest2, needsDicomWebCaching2, currentDbConfiguration, pluginStatus_.currentlyProcessingConfiguration);
      needsFullProcessing = needsReconstruct2 || needsReingest2 || needsDicomWebCaching2;  // if the configuration has changed compared to the config being processed, we need a full processing again
    }
  }

  if (!needsProcessing && !force_)
  {
    ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: everything has been processed already !");
    return;
  }

  if (force_ || needsFullProcessing)
  {
    if (force_)
    {
      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: forcing execution -> will perform housekeeping");
    }
    else
    {
      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: the DB configuration has changed since last run, will reprocess the whole DB !");
    }
    
    Json::Value changes;
    OrthancPlugins::RestApiGet(changes, "/changes?last", false);

    {
      boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);

      pluginStatus_.lastProcessedChange = 0;
      pluginStatus_.lastChangeToProcess = changes["Last"].asInt64();  // the last change is the last change at the time we start.  We assume that every new ingested file will be constructed correctly
      pluginStatus_.lastTimeStarted = boost::posix_time::microsec_clock::universal_time();
    }
  }
  else
  {
    ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: the DB configuration has not changed since last run, will continue processing changes");
  }

  bool completed = false;
  {
    boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);
    completed = pluginStatus_.lastChangeToProcess == 0;  // if the DB is empty at start, no need to process anyting
  }

  bool loggedNotRightPeriodChangeMessage = false;

  while (!workerThreadShouldStop_ && !completed)
  {
    if (runningPeriods_.isInPeriod())
    {
      completed = ProcessChanges(needsReconstruct, needsReingest, needsDicomWebCaching, currentDbConfiguration);
      SaveStatusInDb();
      
      if (!completed)
      {
        boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);
    
        ORTHANC_PLUGINS_LOG_INFO("Housekeeper: processed changes " + boost::lexical_cast<std::string>(pluginStatus_.lastProcessedChange) +
                                 " / " + boost::lexical_cast<std::string>(pluginStatus_.lastChangeToProcess));
        
        boost::this_thread::sleep(boost::posix_time::milliseconds(throttleDelay_ * 100));  // wait 1/10 of the delay between changes
      }

      loggedNotRightPeriodChangeMessage = false;
    }
    else
    {
      if (!loggedNotRightPeriodChangeMessage)
      {
        ORTHANC_PLUGINS_LOG_INFO("Housekeeper: entering quiet period");
        loggedNotRightPeriodChangeMessage = true;
      }

      boost::this_thread::sleep(boost::posix_time::milliseconds(1000));
    }
  }  

  if (completed)
  {
    boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);

    pluginStatus_.lastProcessedConfiguration = currentDbConfiguration;
    pluginStatus_.currentlyProcessingConfiguration.Clear();

    pluginStatus_.lastProcessedChange = -1;
    pluginStatus_.lastChangeToProcess = -1;
    
    SaveStatusInDb();

    OrthancPluginLogWarning(OrthancPlugins::GetGlobalContext(), "Housekeeper: finished processing all changes");
  }
}

extern "C"
{
  OrthancPluginErrorCode GetPluginStatus(OrthancPluginRestOutput* output,
                                         const char* url,
                                         const OrthancPluginHttpRequest* request)
  {
    if (request->method != OrthancPluginHttpMethod_Get)
    {
      OrthancPlugins::AnswerMethodNotAllowed(output, "GET");
    }
    else
    {
      boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);

      Json::Value status;
      pluginStatus_.ToJson(status);

      OrthancPlugins::AnswerJson(status, output);
    }

    return OrthancPluginErrorCode_Success;
  }


  OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType,
                                          OrthancPluginResourceType resourceType,
                                          const char* resourceId)
  {
    switch (changeType)
    {
      case OrthancPluginChangeType_OrthancStarted:
      {
        workerThread_.reset(new boost::thread(WorkerThread));
        return OrthancPluginErrorCode_Success;
      }
      case OrthancPluginChangeType_OrthancStopped:
      {
        if (workerThread_ && workerThread_->joinable())
        {
          workerThreadShouldStop_ = true;
          workerThread_->join();
        }
      }
      default:
        return OrthancPluginErrorCode_Success;
    }
  }

  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
  {
    OrthancPlugins::SetGlobalContext(c, HOUSEKEEPER_NAME);

    /* Check the version of the Orthanc core */
    if (OrthancPluginCheckVersion(c) == 0)
    {
      OrthancPlugins::ReportMinimalOrthancVersion(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
                                                  ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
                                                  ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
      return -1;
    }

    ORTHANC_PLUGINS_LOG_WARNING("Housekeeper plugin is initializing");
    OrthancPluginSetDescription2(c, HOUSEKEEPER_NAME, "Optimizes your DB and storage.");

    OrthancPlugins::OrthancConfiguration orthancConfiguration;

    OrthancPlugins::OrthancConfiguration housekeeper;
    orthancConfiguration.GetSection(housekeeper, "Housekeeper");

    bool enabled = housekeeper.GetBooleanValue("Enable", false);
    if (enabled)
    {
      /*
        {
          "Housekeeper": {
            
            // Enables/disables the plugin
            "Enable": false,

            // the Global Prooperty ID in which the plugin progress
            // is stored.  Must be > 1024 and must not be used by
            // another plugin
            "GlobalPropertyId": 1025,

            // Forces execution even if the plugin did not detect
            // any changes in configuration
            "Force": false,

            // Delay (in seconds) between reconstruction of 2 studies
            // This avoids overloading Orthanc with the housekeeping
            // process and leaves room for other operations.
            "ThrottleDelay": 5,

            // Runs the plugin only at certain period of time.
            // If not specified, the plugin runs all the time
            // Examples: 
            // to run between 0AM and 6AM everyday + every night 
            // from 8PM to 12PM and 24h a day on the weekend:
            // "Schedule": {
            //   "Monday": ["0-6", "20-24"],
            //   "Tuesday": ["0-6", "20-24"],
            //   "Wednesday": ["0-6", "20-24"],
            //   "Thursday": ["0-6", "20-24"],
            //   "Friday": ["0-6", "20-24"],
            //   "Saturday": ["0-24"],
            //   "Sunday": ["0-24"]
            // },

            // configure events that can trigger a housekeeping processing 
            "Triggers" : {
              "StorageCompressionChange": true,
              "MainDicomTagsChange": true,
              "UnnecessaryDicomAsJsonFiles": true,
              "DicomWebCacheChange": true   // new in 1.12.2
            },

            // When rebuilding MainDicomTags, limit to a single level of resource.
            // Allowed values: "Patient", "Study", "Series", "Instance"
            "LimitMainDicomTagsReconstructLevel": "Study"

          }
        }
      */


      globalPropertyId_ = housekeeper.GetIntegerValue("GlobalPropertyId", 1025);
      force_ = housekeeper.GetBooleanValue("Force", false);
      throttleDelay_ = housekeeper.GetUnsignedIntegerValue("ThrottleDelay", 5);      

      if (housekeeper.GetJson().isMember("Triggers"))
      {
        OrthancPlugins::OrthancConfiguration triggers;
        housekeeper.GetSection(triggers, "Triggers");
        triggerOnStorageCompressionChange_ = triggers.GetBooleanValue("StorageCompressionChange", true);

        triggerOnMainDicomTagsChange_ = triggers.GetBooleanValue("MainDicomTagsChange", true);
        triggerOnUnnecessaryDicomAsJsonFiles_ = triggers.GetBooleanValue("UnnecessaryDicomAsJsonFiles", true);
        triggerOnIngestTranscodingChange_ = triggers.GetBooleanValue("IngestTranscodingChange", true);
        triggerOnDicomWebCacheChange_ = triggers.GetBooleanValue("DicomWebCacheChange", true);
      }

      limitMainDicomTagsReconstructLevel_ = housekeeper.GetStringValue("LimitMainDicomTagsReconstructLevel", "");
      if (limitMainDicomTagsReconstructLevel_ != "Patient" && limitMainDicomTagsReconstructLevel_ != "Study"
        && limitMainDicomTagsReconstructLevel_ != "Series" && limitMainDicomTagsReconstructLevel_ != "Instance")
      {
        ORTHANC_PLUGINS_LOG_ERROR("Housekeeper invalid value for 'LimitMainDicomTagsReconstructLevel': '" + limitMainDicomTagsReconstructLevel_ + "'");
      }
      else if (limitMainDicomTagsReconstructLevel_ == "Patient")
      {
        limitToChange_ = "NewPatient";
        limitToUrl_ = "patients";
      }
      else if (limitMainDicomTagsReconstructLevel_ == "Study")
      {
        limitToChange_ = "NewStudy";
        limitToUrl_ = "studies";
      }
      else if (limitMainDicomTagsReconstructLevel_ == "Series")
      {
        limitToChange_ = "NewSeries";
        limitToUrl_ = "series";
      }
      else if (limitMainDicomTagsReconstructLevel_ == "Instance")
      {
        limitToChange_ = "NewInstance";
        limitToUrl_ = "instances";
      }

      if (housekeeper.GetJson().isMember("Schedule"))
      {
        runningPeriods_.load(housekeeper.GetJson()["Schedule"]);
      }

      OrthancPluginRegisterOnChangeCallback(c, OnChangeCallback);
      OrthancPluginRegisterRestCallback(c, "/housekeeper/status", GetPluginStatus);   // for backward compatiblity with version 1.11.0
      OrthancPluginRegisterRestCallback(c, "/plugins/housekeeper/status", GetPluginStatus);
    }
    else
    {
      ORTHANC_PLUGINS_LOG_WARNING("Housekeeper plugin is disabled by the configuration file");
    }

    return 0;
  }


  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
  {
    ORTHANC_PLUGINS_LOG_WARNING("Housekeeper plugin is finalizing");
  }


  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
  {
    return HOUSEKEEPER_NAME;
  }


  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
  {
    return HOUSEKEEPER_VERSION;
  }
}