diff Applications/Platforms/WebAssembly/WebAssemblyOracle.cpp @ 1591:5887a4f8594b

moving platform-specific files out of the "OrthancStone" folder
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 23 Oct 2020 13:15:03 +0200
parents OrthancStone/Sources/Oracle/WebAssemblyOracle.cpp@1b3039384972
children 4fb8fdf03314
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/Platforms/WebAssembly/WebAssemblyOracle.cpp	Fri Oct 23 13:15:03 2020 +0200
@@ -0,0 +1,818 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero 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
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#if defined(ORTHANC_BUILDING_STONE_LIBRARY) && ORTHANC_BUILDING_STONE_LIBRARY == 1
+#  include "WebAssemblyOracle_Includes.h"
+#else
+// This is the case when using the WebAssembly side module, and this
+// source file must be compiled within the WebAssembly main module
+#  include <Oracle/WebAssemblyOracle_Includes.h>
+#endif
+
+#include <OrthancException.h>
+#include <Toolbox.h>
+
+#include <emscripten.h>
+#include <emscripten/html5.h>
+#include <emscripten/fetch.h>
+
+
+#if ORTHANC_ENABLE_DCMTK == 1
+static unsigned int BUCKET_SOP = 1;
+#endif
+
+
+namespace OrthancStone
+{
+  class WebAssemblyOracle::TimeoutContext
+  {
+  private:
+    WebAssemblyOracle&                 oracle_;
+    boost::weak_ptr<IObserver>         receiver_;
+    std::unique_ptr<SleepOracleCommand>  command_;
+
+  public:
+    TimeoutContext(WebAssemblyOracle& oracle,
+                   boost::weak_ptr<IObserver> receiver,
+                   IOracleCommand* command) :
+      oracle_(oracle),
+      receiver_(receiver)
+    {
+      if (command == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+      else
+      {
+        command_.reset(dynamic_cast<SleepOracleCommand*>(command));
+      }
+    }
+
+    void EmitMessage()
+    {      
+      assert(command_.get() != NULL);
+
+      SleepOracleCommand::TimeoutMessage message(*command_);
+      oracle_.EmitMessage(receiver_, message);
+    }
+
+    static void Callback(void *userData)
+    {
+      std::unique_ptr<TimeoutContext> context(reinterpret_cast<TimeoutContext*>(userData));
+      context->EmitMessage();
+    }
+  };
+    
+
+  /**
+  This object is created on the heap for every http request.
+  It is deleted in the success (or error) callbacks.
+
+  This object references the receiver of the request. Since this is a raw
+  reference, we need additional checks to make sure we send the response to
+  the same object, for the object can be deleted and a new one recreated at the
+  same address (it often happens in the [single-threaded] browser context).
+  */
+  class WebAssemblyOracle::FetchContext : public boost::noncopyable
+  {
+  private:
+    WebAssemblyOracle&             oracle_;
+    boost::weak_ptr<IObserver>     receiver_;
+    std::unique_ptr<IOracleCommand>  command_;
+    std::string                    expectedContentType_;
+
+  public:
+    FetchContext(WebAssemblyOracle& oracle,
+                 boost::weak_ptr<IObserver> receiver,
+                 IOracleCommand* command,
+                 const std::string& expectedContentType) :
+      oracle_(oracle),
+      receiver_(receiver),
+      command_(command),
+      expectedContentType_(expectedContentType)
+    {
+      if (Orthanc::Logging::IsTraceLevelEnabled())
+      {
+        // Calling "receiver.lock()" is expensive, hence the quick check if TRACE is enabled
+        LOG(TRACE) << "WebAssemblyOracle::FetchContext::FetchContext() | "
+                   << "receiver address = " << std::hex << receiver.lock().get();
+      }
+
+      if (command == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+    }
+
+    const std::string& GetExpectedContentType() const
+    {
+      return expectedContentType_;
+    }
+
+    IMessageEmitter& GetEmitter() const
+    {
+      return oracle_;
+    }
+
+    boost::weak_ptr<IObserver> GetReceiver() const
+    {
+      return receiver_;
+    }
+
+    void EmitMessage(const IMessage& message)
+    {
+      if (Orthanc::Logging::IsTraceLevelEnabled())
+      {
+        // Calling "receiver_.lock()" is expensive, hence the quick check if TRACE is enabled
+        LOG(TRACE) << "WebAssemblyOracle::FetchContext::EmitMessage receiver_ = "
+                   << std::hex << receiver_.lock().get() << std::dec;
+      }
+      
+      oracle_.EmitMessage(receiver_, message);
+    }
+
+    IOracleCommand& GetCommand() const
+    {
+      return *command_;
+    }
+
+    template <typename T>
+    const T& GetTypedCommand() const
+    {
+      return dynamic_cast<T&>(*command_);
+    }
+
+#if ORTHANC_ENABLE_DCMTK == 1
+    void StoreInCache(const std::string& sopInstanceUid,
+                      std::unique_ptr<Orthanc::ParsedDicomFile>& dicom,
+                      size_t fileSize)
+    {
+      if (oracle_.dicomCache_.get())
+      {
+        // Store it into the cache for future use
+        oracle_.dicomCache_->Acquire(BUCKET_SOP, sopInstanceUid,
+                                     dicom.release(), fileSize, true);
+      }              
+    }
+#endif
+
+    static void SuccessCallback(emscripten_fetch_t *fetch)
+    {
+      /**
+       * Firstly, make a local copy of the fetched information, and
+       * free data associated with the fetch.
+       **/
+      
+      if (fetch->userData == NULL)
+      {
+        LOG(ERROR) << "WebAssemblyOracle::FetchContext::SuccessCallback fetch->userData is NULL!!!!!!!";
+        return;
+      }
+
+      std::unique_ptr<FetchContext> context(reinterpret_cast<FetchContext*>(fetch->userData));
+
+      std::string answer;
+      if (fetch->numBytes > 0)
+      {
+        answer.assign(fetch->data, fetch->numBytes);
+      }
+
+
+      /**
+       * Retrieving the headers of the HTTP answer.
+       **/
+      HttpHeaders headers;
+
+#if (__EMSCRIPTEN_major__ < 1 ||                                        \
+     (__EMSCRIPTEN_major__ == 1 && __EMSCRIPTEN_minor__ < 38) ||        \
+     (__EMSCRIPTEN_major__ == 1 && __EMSCRIPTEN_minor__ == 38 && __EMSCRIPTEN_tiny__ < 37))
+#  warning Consider upgrading Emscripten to a version above 1.38.37, incomplete support of Fetch API
+
+      /**
+       * HACK - If emscripten < 1.38.37, the fetch API does not
+       * contain a way to retrieve the HTTP headers of the answer. We
+       * make the assumption that the "Content-Type" header of the
+       * response is the same as the "Accept" header of the
+       * query. This is fixed thanks to the
+       * "emscripten_fetch_get_response_headers()" function that was
+       * added to "fetch.h" at emscripten-1.38.37 on 2019-06-26.
+       *
+       * https://github.com/emscripten-core/emscripten/blob/1.38.37/system/include/emscripten/fetch.h
+       * https://github.com/emscripten-core/emscripten/pull/8486
+       **/
+      if (fetch->userData != NULL)
+      {
+        if (!context->GetExpectedContentType().empty())
+        {
+          headers["Content-Type"] = context->GetExpectedContentType();
+        }
+      }
+#else
+      {
+        size_t size = emscripten_fetch_get_response_headers_length(fetch);
+
+        std::string plainHeaders(size + 1, '\0');
+        emscripten_fetch_get_response_headers(fetch, &plainHeaders[0], size + 1);
+
+        std::vector<std::string> tokens;
+        Orthanc::Toolbox::TokenizeString(tokens, plainHeaders, '\n');
+
+        for (size_t i = 0; i < tokens.size(); i++)
+        {
+          size_t p = tokens[i].find(':');
+          if (p != std::string::npos)
+          {
+            std::string key = Orthanc::Toolbox::StripSpaces(tokens[i].substr(0, p));
+            std::string value = Orthanc::Toolbox::StripSpaces(tokens[i].substr(p + 1));
+            headers[key] = value;
+          }
+        }
+      }
+#endif
+      
+      LOG(TRACE) << "About to call emscripten_fetch_close";
+      emscripten_fetch_close(fetch);
+      LOG(TRACE) << "Successfully called emscripten_fetch_close";
+
+      /**
+       * Secondly, use the retrieved data.
+       * IMPORTANT NOTE: the receiver might be dead. This is prevented 
+       * by the object responsible for zombie check, later on.
+       **/
+      try
+      {
+        if (context.get() == NULL)
+        {
+          LOG(ERROR) << "WebAssemblyOracle::FetchContext::SuccessCallback: (context.get() == NULL)";
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+        }
+        else
+        {
+          switch (context->GetCommand().GetType())
+          {
+            case IOracleCommand::Type_Http:
+            {
+              HttpCommand::SuccessMessage message(context->GetTypedCommand<HttpCommand>(), headers, answer);
+              context->EmitMessage(message);
+              break;
+            }
+
+            case IOracleCommand::Type_OrthancRestApi:
+            {
+              LOG(TRACE) << "WebAssemblyOracle::FetchContext::SuccessCallback. About to call context->EmitMessage(message);";
+              OrthancRestApiCommand::SuccessMessage message
+                (context->GetTypedCommand<OrthancRestApiCommand>(), headers, answer);
+              context->EmitMessage(message);
+              break;
+            }
+
+            case IOracleCommand::Type_GetOrthancImage:
+            {
+              context->GetTypedCommand<GetOrthancImageCommand>().ProcessHttpAnswer
+                (context->GetReceiver(), context->GetEmitter(), answer, headers);
+              break;
+            }
+
+            case IOracleCommand::Type_GetOrthancWebViewerJpeg:
+            {
+              context->GetTypedCommand<GetOrthancWebViewerJpegCommand>().ProcessHttpAnswer
+                (context->GetReceiver(), context->GetEmitter(), answer);
+              break;
+            }
+
+            case IOracleCommand::Type_ParseDicomFromWado:
+            {
+#if ORTHANC_ENABLE_DCMTK == 1
+              const ParseDicomFromWadoCommand& command =
+                context->GetTypedCommand<ParseDicomFromWadoCommand>();
+              
+              size_t fileSize;
+              std::unique_ptr<Orthanc::ParsedDicomFile> dicom
+                (ParseDicomSuccessMessage::ParseWadoAnswer(fileSize, answer, headers));
+
+              {
+                ParseDicomSuccessMessage message(command, command.GetSource(), *dicom, fileSize, true);
+                context->EmitMessage(message);
+              }
+
+              context->StoreInCache(command.GetSopInstanceUid(), dicom, fileSize);
+#else
+              throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+#endif
+              break;
+            }
+
+            default:
+              LOG(ERROR) << "Command type not implemented by the WebAssembly Oracle (in SuccessCallback): "
+                         << context->GetCommand().GetType();
+              throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+          }
+        }
+      }
+      catch (Orthanc::OrthancException& e)
+      {
+        LOG(INFO) << "Error while processing a fetch answer in the oracle: " << e.What();
+
+        {
+          OracleCommandExceptionMessage message(context->GetCommand(), e);
+          context->EmitMessage(message);
+        }
+      }
+    }
+
+    static void FailureCallback(emscripten_fetch_t *fetch)
+    {
+      std::unique_ptr<FetchContext> context(reinterpret_cast<FetchContext*>(fetch->userData));
+
+#if 0
+      {
+        const size_t kEmscriptenStatusTextSize = sizeof(emscripten_fetch_t::statusText);
+        char message[kEmscriptenStatusTextSize + 1];
+        memcpy(message, fetch->statusText, kEmscriptenStatusTextSize);
+        message[kEmscriptenStatusTextSize] = 0;
+
+        LOG(ERROR) << "Fetching " << fetch->url
+                   << " failed, HTTP failure status code: " << fetch->status
+                   << " | statusText = " << message
+                   << " | numBytes = " << fetch->numBytes
+                   << " | totalBytes = " << fetch->totalBytes
+                   << " | readyState = " << fetch->readyState;
+      }
+#endif
+
+      {
+        OracleCommandExceptionMessage message
+          (context->GetCommand(), Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol));
+        context->EmitMessage(message);
+      }
+      
+      /**
+       * TODO - The following code leads to an infinite recursion, at
+       * least with Firefox running on incognito mode => WHY?
+       **/      
+      emscripten_fetch_close(fetch); // Also free data on failure.
+    }
+  };
+    
+
+
+  class WebAssemblyOracle::FetchCommand : public boost::noncopyable
+  {
+  private:
+    WebAssemblyOracle&             oracle_;
+    boost::weak_ptr<IObserver>     receiver_;
+    std::unique_ptr<IOracleCommand>  command_;
+    Orthanc::HttpMethod            method_;
+    std::string                    url_;
+    std::string                    body_;
+    HttpHeaders                    headers_;
+    unsigned int                   timeout_;
+    std::string                    expectedContentType_;
+    bool                           hasCredentials_;
+    std::string                    username_;
+    std::string                    password_;
+
+  public:
+    FetchCommand(WebAssemblyOracle& oracle,
+                 boost::weak_ptr<IObserver> receiver,
+                 IOracleCommand* command) :
+      oracle_(oracle),
+      receiver_(receiver),
+      command_(command),
+      method_(Orthanc::HttpMethod_Get),
+      timeout_(0),
+      hasCredentials_(false)
+    {
+      if (command == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+    }
+
+    void SetMethod(Orthanc::HttpMethod method)
+    {
+      method_ = method;
+    }
+
+    void SetUrl(const std::string& url)
+    {
+      url_ = url;
+    }
+
+    void SetBody(std::string& body /* will be swapped */)
+    {
+      body_.swap(body);
+    }
+
+    void AddHttpHeaders(const HttpHeaders& headers)
+    {
+      for (HttpHeaders::const_iterator it = headers.begin(); it != headers.end(); ++it)
+      {
+        headers_[it->first] = it->second;
+      }
+    }
+
+    void SetTimeout(unsigned int timeout)
+    {
+      timeout_ = timeout;
+    }
+
+    void SetCredentials(const std::string& username,
+                        const std::string& password)
+    {
+      hasCredentials_ = true;
+      username_ = username;
+      password_ = password;
+    }
+
+    void Execute()
+    {
+      if (command_.get() == NULL)
+      {
+        // Cannot call Execute() twice
+        LOG(ERROR) << "WebAssemblyOracle::Execute(): (command_.get() == NULL)";
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+
+      emscripten_fetch_attr_t attr;
+      emscripten_fetch_attr_init(&attr);
+
+      const char* method;
+      
+      switch (method_)
+      {
+        case Orthanc::HttpMethod_Get:
+          method = "GET";
+          break;
+
+        case Orthanc::HttpMethod_Post:
+          method = "POST";
+          break;
+
+        case Orthanc::HttpMethod_Delete:
+          method = "DELETE";
+          break;
+
+        case Orthanc::HttpMethod_Put:
+          method = "PUT";
+          break;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+      }
+
+      strcpy(attr.requestMethod, method);
+
+      attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY | EMSCRIPTEN_FETCH_REPLACE;
+      attr.onsuccess = FetchContext::SuccessCallback;
+      attr.onerror = FetchContext::FailureCallback;
+      attr.timeoutMSecs = timeout_ * 1000;
+
+      if (hasCredentials_)
+      {
+        attr.withCredentials = EM_TRUE;
+        attr.userName = username_.c_str();
+        attr.password = password_.c_str();
+      }
+      
+      std::vector<const char*> headers;
+      headers.reserve(2 * headers_.size() + 1);
+
+      std::string expectedContentType;
+        
+      for (HttpHeaders::const_iterator it = headers_.begin(); it != headers_.end(); ++it)
+      {
+        std::string key;
+        Orthanc::Toolbox::ToLowerCase(key, it->first);
+          
+        if (key == "accept")
+        {
+          expectedContentType = it->second;
+        }
+
+        if (key != "accept-encoding")  // Web browsers forbid the modification of this HTTP header
+        {
+          headers.push_back(it->first.c_str());
+          headers.push_back(it->second.c_str());
+        }
+      }
+        
+      headers.push_back(NULL);  // Termination of the array of HTTP headers
+
+      attr.requestHeaders = &headers[0];
+
+      char* requestData = NULL;
+      if (!body_.empty())
+        requestData = reinterpret_cast<char*>(malloc(body_.size()));
+        
+      try 
+      {
+        if (!body_.empty())
+        {
+          memcpy(requestData, &(body_[0]), body_.size());
+          attr.requestDataSize = body_.size();
+          attr.requestData = requestData;
+        }
+        attr.userData = new FetchContext(oracle_, receiver_, command_.release(), expectedContentType);
+
+        // Must be the last call to prevent memory leak on error
+        emscripten_fetch(&attr, url_.c_str());
+      }        
+      catch(...)
+      {
+        if(requestData != NULL)
+          free(requestData);
+        throw;
+      }
+    }
+  };
+
+
+  void WebAssemblyOracle::SetOrthancUrl(FetchCommand& command,
+                                        const std::string& uri) const
+  {
+    if (isLocalOrthanc_)
+    {
+      command.SetUrl(localOrthancRoot_ + uri);
+    }
+    else
+    {
+      command.SetUrl(remoteOrthanc_.GetUrl() + uri);
+      command.AddHttpHeaders(remoteOrthanc_.GetHttpHeaders());
+      
+      if (!remoteOrthanc_.GetUsername().empty())
+      {
+        command.SetCredentials(remoteOrthanc_.GetUsername(), remoteOrthanc_.GetPassword());
+      }
+    }
+  }
+    
+
+  void WebAssemblyOracle::Execute(boost::weak_ptr<IObserver> receiver,
+                                  HttpCommand* command)
+  {
+    FetchCommand fetch(*this, receiver, command);
+    
+    fetch.SetMethod(command->GetMethod());
+    fetch.SetUrl(command->GetUrl());
+    fetch.AddHttpHeaders(command->GetHttpHeaders());
+    fetch.SetTimeout(command->GetTimeout());
+    
+    if (command->GetMethod() == Orthanc::HttpMethod_Post ||
+        command->GetMethod() == Orthanc::HttpMethod_Put)
+    {
+      std::string body;
+      command->SwapBody(body);
+      fetch.SetBody(body);
+    }
+    
+    fetch.Execute();
+  }
+  
+
+  void WebAssemblyOracle::Execute(boost::weak_ptr<IObserver> receiver,
+                                  OrthancRestApiCommand* command)
+  {
+    try
+    {
+      //LOG(TRACE) << "*********** WebAssemblyOracle::Execute.";
+      //LOG(TRACE) << "WebAssemblyOracle::Execute | command = " << command;
+      FetchCommand fetch(*this, receiver, command);
+
+      fetch.SetMethod(command->GetMethod());
+      SetOrthancUrl(fetch, command->GetUri());
+      fetch.AddHttpHeaders(command->GetHttpHeaders());
+      fetch.SetTimeout(command->GetTimeout());
+
+      if (command->GetMethod() == Orthanc::HttpMethod_Post ||
+        command->GetMethod() == Orthanc::HttpMethod_Put)
+      {
+        std::string body;
+        command->SwapBody(body);
+        fetch.SetBody(body);
+      }
+
+      fetch.Execute();
+      //LOG(TRACE) << "*********** successful end of WebAssemblyOracle::Execute.";
+    }
+    catch (const Orthanc::OrthancException& e)
+    {
+      if (e.HasDetails())
+      {
+        LOG(ERROR) << "OrthancException in WebAssemblyOracle::Execute: " << e.What() << " Details: " << e.GetDetails();
+      }
+      else
+      {
+        LOG(ERROR) << "OrthancException in WebAssemblyOracle::Execute: " << e.What();
+      }
+      //LOG(TRACE) << "*********** failing end of WebAssemblyOracle::Execute.";
+      throw;
+    }
+    catch (const std::exception& e)
+    {
+      LOG(ERROR) << "std::exception in WebAssemblyOracle::Execute: " << e.what();
+//       LOG(TRACE) << "*********** failing end of WebAssemblyOracle::Execute.";
+      throw;
+    }
+    catch (...)
+    {
+      LOG(ERROR) << "Unknown exception in WebAssemblyOracle::Execute";
+//       LOG(TRACE) << "*********** failing end of WebAssemblyOracle::Execute.";
+      throw;
+    }
+  }
+    
+    
+  void WebAssemblyOracle::Execute(boost::weak_ptr<IObserver> receiver,
+                                  GetOrthancImageCommand* command)
+  {
+    FetchCommand fetch(*this, receiver, command);
+
+    SetOrthancUrl(fetch, command->GetUri());
+    fetch.AddHttpHeaders(command->GetHttpHeaders());
+    fetch.SetTimeout(command->GetTimeout());
+      
+    fetch.Execute();
+  }
+    
+    
+  void WebAssemblyOracle::Execute(boost::weak_ptr<IObserver> receiver,
+                                  GetOrthancWebViewerJpegCommand* command)
+  {
+    FetchCommand fetch(*this, receiver, command);
+
+    SetOrthancUrl(fetch, command->GetUri());
+    fetch.AddHttpHeaders(command->GetHttpHeaders());
+    fetch.SetTimeout(command->GetTimeout());
+      
+    fetch.Execute();
+  }
+
+
+  void WebAssemblyOracle::Execute(boost::weak_ptr<IObserver> receiver,
+                                  ParseDicomFromWadoCommand* command)
+  {
+    std::unique_ptr<ParseDicomFromWadoCommand> protection(command);
+    
+#if ORTHANC_ENABLE_DCMTK == 1
+    if (dicomCache_.get())
+    {
+      ParsedDicomCache::Reader reader(*dicomCache_, BUCKET_SOP, protection->GetSopInstanceUid());
+      if (reader.IsValid() &&
+          reader.HasPixelData())
+      {
+        // Reuse the DICOM file from the cache
+        ParseDicomSuccessMessage message(*protection, protection->GetSource(), reader.GetDicom(),
+                                         reader.GetFileSize(), reader.HasPixelData());
+        EmitMessage(receiver, message);
+        return;
+      }
+    }
+#endif
+
+    switch (command->GetRestCommand().GetType())
+    {
+      case IOracleCommand::Type_Http:
+      {
+        const HttpCommand& rest =
+          dynamic_cast<const HttpCommand&>(protection->GetRestCommand());
+        
+        FetchCommand fetch(*this, receiver, protection.release());
+    
+        fetch.SetMethod(rest.GetMethod());
+        fetch.SetUrl(rest.GetUrl());
+        fetch.AddHttpHeaders(rest.GetHttpHeaders());
+        fetch.SetTimeout(rest.GetTimeout());
+    
+        if (rest.GetMethod() == Orthanc::HttpMethod_Post ||
+            rest.GetMethod() == Orthanc::HttpMethod_Put)
+        {
+          std::string body = rest.GetBody();
+          fetch.SetBody(body);
+        }
+    
+        fetch.Execute();
+        break;
+      }
+
+      case IOracleCommand::Type_OrthancRestApi:
+      {
+        const OrthancRestApiCommand& rest =
+          dynamic_cast<const OrthancRestApiCommand&>(protection->GetRestCommand());
+        
+        FetchCommand fetch(*this, receiver, protection.release());
+
+        fetch.SetMethod(rest.GetMethod());
+        SetOrthancUrl(fetch, rest.GetUri());
+        fetch.AddHttpHeaders(rest.GetHttpHeaders());
+        fetch.SetTimeout(rest.GetTimeout());
+
+        if (rest.GetMethod() == Orthanc::HttpMethod_Post ||
+            rest.GetMethod() == Orthanc::HttpMethod_Put)
+        {
+          std::string body = rest.GetBody();
+          fetch.SetBody(body);
+        }
+
+        fetch.Execute();
+        break;
+      }
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+  }
+
+
+  bool WebAssemblyOracle::Schedule(boost::shared_ptr<IObserver> receiver,
+                                   IOracleCommand* command)
+  {
+    LOG(TRACE) << "WebAssemblyOracle::Schedule : receiver = "
+               << std::hex << receiver.get();
+    
+    std::unique_ptr<IOracleCommand> protection(command);
+
+    if (command == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+
+    switch (command->GetType())
+    {
+      case IOracleCommand::Type_Http:
+        Execute(receiver, dynamic_cast<HttpCommand*>(protection.release()));
+        break;
+        
+      case IOracleCommand::Type_OrthancRestApi:
+        Execute(receiver, dynamic_cast<OrthancRestApiCommand*>(protection.release()));
+        break;
+        
+      case IOracleCommand::Type_GetOrthancImage:
+        Execute(receiver, dynamic_cast<GetOrthancImageCommand*>(protection.release()));
+        break;
+
+      case IOracleCommand::Type_GetOrthancWebViewerJpeg:
+        Execute(receiver, dynamic_cast<GetOrthancWebViewerJpegCommand*>(protection.release()));
+        break;          
+            
+      case IOracleCommand::Type_Sleep:
+      {
+        unsigned int timeoutMS = dynamic_cast<SleepOracleCommand*>(command)->GetDelay();
+        emscripten_set_timeout(TimeoutContext::Callback, timeoutMS,
+                               new TimeoutContext(*this, receiver, protection.release()));
+        break;
+      }
+            
+      case IOracleCommand::Type_ParseDicomFromWado:
+#if ORTHANC_ENABLE_DCMTK == 1
+        Execute(receiver, dynamic_cast<ParseDicomFromWadoCommand*>(protection.release()));
+#else
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented,
+                                          "DCMTK must be enabled to parse DICOM files");
+#endif
+        break;
+            
+      default:
+        LOG(ERROR) << "Command type not implemented by the WebAssembly Oracle (in Schedule): "
+                   << command->GetType();
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+
+    return true;
+  }
+
+
+  void WebAssemblyOracle::SetDicomCacheSize(size_t size)
+  {
+#if ORTHANC_ENABLE_DCMTK == 1
+    if (size == 0)
+    {
+      dicomCache_.reset();
+    }
+    else
+    {
+      dicomCache_.reset(new ParsedDicomCache(size));
+    }
+#else
+    LOG(INFO) << "DCMTK support is disabled, the DICOM cache is disabled";
+#endif
+  }
+}