Mercurial > hg > orthanc-wsi
view ViewerPlugin/Annotations/AnnotationsWorkspace.cpp @ 613:617f25e606d6 default tip
back to mainline
| author | Sebastien Jodogne <s.jodogne@gmail.com> |
|---|---|
| date | Thu, 10 Sep 2026 10:35:58 +0200 |
| parents | 11f795017763 |
| children |
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-2026 Orthanc Team SRL, Belgium * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, 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/>. **/ #include "../../Framework/PrecompiledHeadersWSI.h" #include "AnnotationsWorkspace.h" #include "../ViewerConfiguration.h" #include "../ViewerToolbox.h" #include <OrthancException.h> #include <boost/regex.hpp> static const char* const KEY_ACTIVE_INSTRUCTORS = "active-instructors"; static const char* const KEY_ACTIVE_LEARNERS = "active-learners"; namespace OrthancWSI { class AnnotationsWorkspace::PersistentInfo : public ISerializable { private: // An active user is always of type "standard" std::set<UserId> activeInstructors_; std::set<UserId> activeLearners_; static void ParseActiveUsers(std::set<UserId>& target, const Json::Value& serialized) { if (!serialized.isArray()) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); } target.clear(); for (Json::Value::ArrayIndex i = 0; i < serialized.size(); i++) { target.insert(UserId(serialized[i])); } } static void SerializeActiveUsers(Json::Value& serialized, const std::set<UserId>& source) { serialized = Json::arrayValue; for (std::set<UserId>::const_iterator it = source.begin(); it != source.end(); ++it) { Json::Value user; it->Serialize(user); serialized.append(user); } } void SanityCheck() const { #if !defined(NDEBUG) for (std::set<UserId>::const_iterator it = activeInstructors_.begin(); it != activeInstructors_.end(); ++it) { assert(activeLearners_.find(*it) == activeLearners_.end()); } for (std::set<UserId>::const_iterator it = activeLearners_.begin(); it != activeLearners_.end(); ++it) { assert(activeInstructors_.find(*it) == activeInstructors_.end()); } #endif } public: PersistentInfo() { } explicit PersistentInfo(const Json::Value& serialized) { if (!serialized.isObject() || !serialized.isMember(KEY_ACTIVE_INSTRUCTORS) || !serialized.isMember(KEY_ACTIVE_LEARNERS)) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); } ParseActiveUsers(activeInstructors_, serialized[KEY_ACTIVE_INSTRUCTORS]); ParseActiveUsers(activeLearners_, serialized[KEY_ACTIVE_LEARNERS]); SanityCheck(); } // Return "true" iff. the user was not already tagged as active or // if the user has changed their role in the project (from learner // to instructor, or from instructor to learner) bool AddActiveUser(const UserId& user, ProjectRole role) { SanityCheck(); switch (role) { case ProjectRole_Instructor: if (activeInstructors_.find(user) == activeInstructors_.end()) { activeLearners_.erase(user); // Accomodate with change in the role activeInstructors_.insert(user); SanityCheck(); return true; } else { return false; } case ProjectRole_Learner: if (activeLearners_.find(user) == activeLearners_.end()) { activeInstructors_.erase(user); // Accomodate with change in the role activeLearners_.insert(user); SanityCheck(); return true; } else { return false; } break; default: throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } } bool LookupUserRole(ProjectRole& role, const UserId& user) const { if (activeInstructors_.find(user) != activeInstructors_.end()) { assert(activeLearners_.find(user) == activeLearners_.end()); role = ProjectRole_Instructor; return true; } else if (activeLearners_.find(user) != activeLearners_.end()) { assert(activeInstructors_.find(user) == activeInstructors_.end()); role = ProjectRole_Learner; return true; } else { return false; } } virtual void Serialize(Json::Value& serialized) const ORTHANC_OVERRIDE { serialized = Json::objectValue; SerializeActiveUsers(serialized[KEY_ACTIVE_INSTRUCTORS], activeInstructors_); SerializeActiveUsers(serialized[KEY_ACTIVE_LEARNERS], activeLearners_); } class ActiveUsersIterator : public boost::noncopyable { private: std::set<UserId>::const_iterator instructorsIterator_; std::set<UserId>::const_iterator instructorsEnd_; std::set<UserId>::const_iterator learnersIterator_; std::set<UserId>::const_iterator learnersEnd_; public: explicit ActiveUsersIterator(const PersistentInfo& that) : instructorsIterator_(that.activeInstructors_.begin()), instructorsEnd_(that.activeInstructors_.end()), learnersIterator_(that.activeLearners_.begin()), learnersEnd_(that.activeLearners_.end()) { that.SanityCheck(); } bool IsDone() const { return (instructorsIterator_ == instructorsEnd_ && learnersIterator_ == learnersEnd_); } const UserId& GetUser() const { if (instructorsIterator_ != instructorsEnd_) { return *instructorsIterator_; } else if (learnersIterator_ != learnersEnd_) { return *learnersIterator_; } else { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } } ProjectRole GetRole() const { if (instructorsIterator_ != instructorsEnd_) { return ProjectRole_Instructor; } else if (learnersIterator_ != learnersEnd_) { return ProjectRole_Learner; } else { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } } void Next() { if (instructorsIterator_ != instructorsEnd_) { ++instructorsIterator_; } else if (learnersIterator_ != learnersEnd_) { ++learnersIterator_; } else { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } } }; }; void AnnotationsWorkspace::Load(const UserId& user) { const std::string key = id_.GetSettingsKey(user); Json::Value layers; if (ViewerToolbox::LookupKeyValueStore(layers, key)) { std::unique_ptr<UserAnnotationsSettings> item(new UserAnnotationsSettings(layers)); if (content_.find(user) == content_.end()) // Should never be false { content_[user] = item.release(); } } } bool AnnotationsWorkspace::IsSharedWith(const UserLayer& layer, const UserId& layerAuthorId, const UserId& viewerId, ProjectRole viewerRole) const { assert(persistentInfo_.get() != NULL); ProjectRole authorRole; if (persistentInfo_->LookupUserRole(authorRole, layerAuthorId)) { return layer.IsSharedWith(authorRole, viewerId, viewerRole); } else { return false; } } AnnotationsWorkspace::AnnotationsWorkspace(const AnnotationsWorkspaceId& id) : id_(id), projectInformation_(id.GetProjectId()) { const std::string key = id.GetInfoKey(); Json::Value info; if (ViewerToolbox::LookupKeyValueStore(info, key)) { persistentInfo_.reset(new PersistentInfo(info)); PersistentInfo::ActiveUsersIterator iterator(*persistentInfo_); while (!iterator.IsDone()) { Load(iterator.GetUser()); iterator.Next(); } } else { persistentInfo_.reset(new PersistentInfo); persistentInfo_->Serialize(info); ViewerToolbox::SetKeyValueStore(key, info); } } AnnotationsWorkspace::~AnnotationsWorkspace() { for (Content::iterator it = content_.begin(); it != content_.end(); ++it) { assert(it->second != NULL); delete it->second; } } AnnotationsWorkspace::UserReader::UserReader(AnnotationsWorkspace& that, const UserId& userId, ProjectRole userRole) : lock_(that.mutex_), that_(that), userId_(userId), userRole_(userRole) { Content::const_iterator found = that.content_.find(userId); if (found == that.content_.end()) { userSettings_ = NULL; } else { assert(found->second != NULL); userSettings_ = found->second; } } void AnnotationsWorkspace::UserReader::ListLayers(Json::Value& serialized) const { serialized = Json::objectValue; if (IsValid()) { userSettings_->Serialize(serialized); } else { UserAnnotationsSettings empty; empty.Serialize(serialized); } } void AnnotationsWorkspace::UserReader::ListUsersSharingLayersWithMe(std::set<UserId>& target) const { target.clear(); // Loop over all the users (i.e., all the possible authors) in this workspace for (Content::const_iterator it = that_.content_.begin(); it != that_.content_.end(); ++it) { assert(it->second != NULL); if (!userId_.Equals(it->first)) // Don't add self { LayersCollection::Iterator iterator(it->second->GetUserLayers()); // Loop over all the layers of this user in this workspace while (!iterator.IsDone()) { const UserLayer& layer = dynamic_cast<const UserLayer&>(iterator.GetLayer()); if (that_.IsSharedWith(layer, it->first, userId_, userRole_)) { target.insert(it->first); break; } else { iterator.Next(); } } } } } void AnnotationsWorkspace::UserReader::ListLayersSharedWithMe(Json::Value& target, const UserId& author) const { target = Json::arrayValue; if (IsValid()) { Content::const_iterator found = that_.content_.find(author); if (found != that_.content_.end()) { assert(found->second != NULL); LayersCollection::Iterator iterator(found->second->GetUserLayers()); while (!iterator.IsDone()) { const UserLayer& layer = dynamic_cast<const UserLayer&>(iterator.GetLayer()); if (that_.IsSharedWith(layer, author, userId_, userRole_)) { Json::Value item; layer.Serialize(item); target.append(item); } iterator.Next(); } } } } void AnnotationsWorkspace::UserReader::SearchActiveUsers(std::set<UserId>& target, const std::string& query) const { target.clear(); const boost::regex re(query); PersistentInfo::ActiveUsersIterator iterator(*that_.persistentInfo_); while (!iterator.IsDone()) { if (boost::regex_search(iterator.GetUser().GetName(), re)) { bool add = false; switch (userRole_) { case ProjectRole_Instructor: add = true; break; case ProjectRole_Learner: switch (iterator.GetRole()) // Consider the role of the other user { case ProjectRole_Instructor: // Learners can always share with instructors add = true; break; case ProjectRole_Learner: add = ViewerConfiguration::GetInstance().IsLearnerToLearnerSharingEnabled(); break; default: throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); } break; case ProjectRole_Guest: throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess); default: throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); } if (add) { target.insert(iterator.GetUser()); } } iterator.Next(); } } void AnnotationsWorkspace::UserReader::ListImportedLayers(std::set<UserId>& authors, std::set<std::string>& layerIds) const { authors.clear(); layerIds.clear(); if (IsValid()) { LayersCollection::Iterator iterator(userSettings_->GetImportedLayers()); while (!iterator.IsDone()) { const ImportedLayer& layer = dynamic_cast<const ImportedLayer&>(iterator.GetLayer()); Content::const_iterator found = that_.content_.find(layer.GetAuthor()); if (found != that_.content_.end()) { assert(found->second != NULL); // Check that the layer has not been deleted in the meantime by its author, // and that the layer is still shared with this user if (found->second->GetUserLayers().HasLayer(layer.GetId())) { const UserLayer& authorLayer = found->second->GetUserLayer(layer.GetId()); if (that_.IsSharedWith(authorLayer, layer.GetAuthor(), userId_, userRole_)) { authors.insert(layer.GetAuthor()); layerIds.insert(layer.GetId()); } } } iterator.Next(); } } } void AnnotationsWorkspace::UserWriter::Commit() { ISerializable::SetKeyValueStore(that_.id_.GetSettingsKey(userId_), *userSettings_); } AnnotationsWorkspace::UserWriter::UserWriter(AnnotationsWorkspace& that, const UserId& userId, ProjectRole userRole) : lock_(that.mutex_), that_(that), userId_(userId), userRole_(userRole) { if (that.persistentInfo_->AddActiveUser(userId_, userRole)) { // Only update the key-value store if this is the first time we // meet this user or if their role has changed in the project ISerializable::SetKeyValueStore(that.id_.GetInfoKey(), *that.persistentInfo_); } Content::iterator found = that.content_.find(userId_); if (found == that.content_.end()) { std::unique_ptr<UserAnnotationsSettings> layers(new UserAnnotationsSettings); userSettings_ = layers.get(); that.content_[userId_] = layers.release(); Commit(); } else { assert(found->second != NULL); userSettings_ = found->second; } } void AnnotationsWorkspace::UserWriter::CreateUserLayer(Json::Value& answer) { assert(userSettings_ != NULL); const std::string layerId = userSettings_->CreateUserLayer(); Commit(); const UserLayer& layer = userSettings_->GetUserLayer(layerId); layer.Serialize(answer); } void AnnotationsWorkspace::UserWriter::UpdateUserLayer(const UserLayer& updated) { assert(userSettings_ != NULL); UserLayer& layer = userSettings_->GetUserLayer(updated.GetId()); layer.Assign(updated); Commit(); } void AnnotationsWorkspace::UserWriter::DeleteUserLayer(const std::string& layerId) { assert(userSettings_ != NULL); userSettings_->GetUserLayers().DeleteLayer(layerId); Commit(); } void AnnotationsWorkspace::UserWriter::ImportLayer(const UserId& author, const std::string& layerId) { assert(userSettings_ != NULL); Content::const_iterator found = that_.content_.find(author); if (found == that_.content_.end()) { throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); } assert(found->second != NULL); const UserAnnotationsSettings& authorData = *found->second; const UserLayer& layer = authorData.GetUserLayer(layerId); if (!that_.IsSharedWith(layer, author, userId_, userRole_)) { throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess); } userSettings_->ImportLayer(author, layer); Commit(); } void AnnotationsWorkspace::UserWriter::RemoveImportedLayer(const std::string& layerId) { assert(userSettings_ != NULL); userSettings_->GetImportedLayers().DeleteLayer(layerId); Commit(); } void AnnotationsWorkspace::UserWriter::UpdateImportedLayer(const ImportedLayer& updated) { assert(userSettings_ != NULL); ImportedLayer& layer = userSettings_->GetImportedLayer(updated.GetId()); layer.Assign(updated); Commit(); } }
