# HG changeset patch # User Sebastien Jodogne # Date 1608722463 -3600 # Node ID 80fd140b12ba26e5385c04eff76e841e70bc50fb # Parent 38c22715bb568120769d24d8fe6acd7ac64ddb67 New command-line option: "--openapi" to write the OpenAPI documentation of the REST API to a file diff -r 38c22715bb56 -r 80fd140b12ba NEWS --- a/NEWS Tue Dec 22 09:39:06 2020 +0100 +++ b/NEWS Wed Dec 23 12:21:03 2020 +0100 @@ -1,6 +1,7 @@ Pending changes in the mainline =============================== +* New command-line option: "--openapi" to write the OpenAPI documentation of the REST API to a file * Upgraded dependencies for static builds (notably on Windows): - jsoncpp 1.9.4 diff -r 38c22715bb56 -r 80fd140b12ba OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake --- a/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake Tue Dec 22 09:39:06 2020 +0100 +++ b/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake Wed Dec 23 12:21:03 2020 +0100 @@ -311,6 +311,7 @@ ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/StringHttpOutput.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/RestApi/RestApi.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/RestApi/RestApiCall.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/RestApi/RestApiCallDocumentation.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/RestApi/RestApiGetCall.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/RestApi/RestApiOutput.cpp ) diff -r 38c22715bb56 -r 80fd140b12ba OrthancFramework/Sources/Enumerations.cpp --- a/OrthancFramework/Sources/Enumerations.cpp Tue Dec 22 09:39:06 2020 +0100 +++ b/OrthancFramework/Sources/Enumerations.cpp Wed Dec 23 12:21:03 2020 +0100 @@ -2282,6 +2282,97 @@ LOG(INFO) << "Default encoding for DICOM was changed to: " << name; } + + + const char* GetResourceTypeText(ResourceType type, + bool isPlural, + bool isUpperCase) + { + if (isPlural && !isUpperCase) + { + switch (type) + { + case ResourceType_Patient: + return "patients"; + + case ResourceType_Study: + return "studies"; + + case ResourceType_Series: + return "series"; + + case ResourceType_Instance: + return "instances"; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + else if (isPlural && isUpperCase) + { + switch (type) + { + case ResourceType_Patient: + return "Patients"; + + case ResourceType_Study: + return "Studies"; + + case ResourceType_Series: + return "Series"; + + case ResourceType_Instance: + return "Instances"; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + else if (!isPlural && !isUpperCase) + { + switch (type) + { + case ResourceType_Patient: + return "patient"; + + case ResourceType_Study: + return "study"; + + case ResourceType_Series: + return "series"; + + case ResourceType_Instance: + return "instance"; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + else if (!isPlural && isUpperCase) + { + switch (type) + { + case ResourceType_Patient: + return "Patient"; + + case ResourceType_Study: + return "Study"; + + case ResourceType_Series: + return "Series"; + + case ResourceType_Instance: + return "Instance"; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } } diff -r 38c22715bb56 -r 80fd140b12ba OrthancFramework/Sources/Enumerations.h --- a/OrthancFramework/Sources/Enumerations.h Tue Dec 22 09:39:06 2020 +0100 +++ b/OrthancFramework/Sources/Enumerations.h Wed Dec 23 12:21:03 2020 +0100 @@ -499,7 +499,8 @@ RequestOrigin_RestApi, RequestOrigin_Plugins, RequestOrigin_Lua, - RequestOrigin_WebDav // New in Orthanc 1.8.0 + RequestOrigin_WebDav, // New in Orthanc 1.8.0 + RequestOrigin_Documentation // New in Orthanc in Orthanc 1.8.3 for API documentation (OpenAPI) }; enum ServerBarrierEvent @@ -885,4 +886,9 @@ ORTHANC_PUBLIC bool LookupTransferSyntax(DicomTransferSyntax& target, const std::string& uid); + + ORTHANC_PUBLIC + const char* GetResourceTypeText(ResourceType type, + bool isPlural, + bool isLowerCase); } diff -r 38c22715bb56 -r 80fd140b12ba OrthancFramework/Sources/RestApi/RestApi.cpp --- a/OrthancFramework/Sources/RestApi/RestApi.cpp Tue Dec 22 09:39:06 2020 +0100 +++ b/OrthancFramework/Sources/RestApi/RestApi.cpp Wed Dec 23 12:21:03 2020 +0100 @@ -23,7 +23,9 @@ #include "../PrecompiledHeaders.h" #include "RestApi.h" +#include "../HttpServer/StringHttpOutput.h" #include "../Logging.h" +#include "../OrthancException.h" #include // To define "_exit()" under Windows #include @@ -73,10 +75,11 @@ virtual bool Visit(const RestApiHierarchy::Resource& resource, const UriComponents& uri, + bool hasTrailing, const HttpToolbox::Arguments& components, const UriComponents& trailing) { - if (resource.HasHandler(method_)) + if (resource.HasMethod(method_)) { switch (method_) { @@ -120,6 +123,190 @@ return false; } }; + + + + class OpenApiVisitor : public RestApiHierarchy::IVisitor + { + private: + RestApi& restApi_; + Json::Value paths_; + + public: + OpenApiVisitor(RestApi& restApi) : + restApi_(restApi) + { + } + + virtual bool Visit(const RestApiHierarchy::Resource& resource, + const UriComponents& uri, + bool hasTrailing, + const HttpToolbox::Arguments& components, + const UriComponents& trailing) + { + const std::string path = Toolbox::FlattenUri(uri); + + if (hasTrailing) + LOG(WARNING) << ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> " << path; + + if (paths_.isMember(path)) + { + throw OrthancException(ErrorCode_InternalError); + } + + //if (path == "/patients/{id}/protected") + //asm("int $3"); + + if (resource.HasMethod(HttpMethod_Get)) + { + StringHttpOutput o1; + HttpOutput o2(o1, false); + RestApiOutput o3(o2, HttpMethod_Get); + RestApiGetCall call(o3, restApi_, RequestOrigin_Documentation, "" /* remote IP */, + "" /* username */, HttpToolbox::Arguments() /* HTTP headers */, + HttpToolbox::Arguments() /* URI components */, + UriComponents() /* trailing */, + uri, HttpToolbox::Arguments() /* GET arguments */); + + bool ok = false; + Json::Value v; + + try + { + ok = (resource.Handle(call) && + call.GetDocumentation().FormatOpenApi(v)); + } + catch (OrthancException&) + { + } + catch (boost::bad_lexical_cast&) + { + } + + if (ok) + { + paths_[path]["get"] = v; + } + else + { + LOG(WARNING) << "Ignoring URI without API documentation: GET " << path; + } + } + + if (resource.HasMethod(HttpMethod_Post)) + { + StringHttpOutput o1; + HttpOutput o2(o1, false); + RestApiOutput o3(o2, HttpMethod_Post); + RestApiPostCall call(o3, restApi_, RequestOrigin_Documentation, "" /* remote IP */, + "" /* username */, HttpToolbox::Arguments() /* HTTP headers */, + HttpToolbox::Arguments() /* URI components */, + UriComponents() /* trailing */, uri, NULL /* body */, 0 /* body size */); + + bool ok = false; + Json::Value v; + + try + { + ok = (resource.Handle(call) && + call.GetDocumentation().FormatOpenApi(v)); + } + catch (OrthancException&) + { + } + catch (boost::bad_lexical_cast&) + { + } + + if (ok) + { + paths_[path]["post"] = v; + } + else + { + LOG(WARNING) << "Ignoring URI without API documentation: POST " << path; + } + } + + if (resource.HasMethod(HttpMethod_Delete)) + { + StringHttpOutput o1; + HttpOutput o2(o1, false); + RestApiOutput o3(o2, HttpMethod_Delete); + RestApiDeleteCall call(o3, restApi_, RequestOrigin_Documentation, "" /* remote IP */, + "" /* username */, HttpToolbox::Arguments() /* HTTP headers */, + HttpToolbox::Arguments() /* URI components */, + UriComponents() /* trailing */, uri); + + bool ok = false; + Json::Value v; + + try + { + ok = (resource.Handle(call) && + call.GetDocumentation().FormatOpenApi(v)); + } + catch (OrthancException&) + { + } + catch (boost::bad_lexical_cast&) + { + } + + if (ok) + { + paths_[path]["delete"] = v; + } + else + { + LOG(WARNING) << "Ignoring URI without API documentation: DELETE " << path; + } + } + + if (resource.HasMethod(HttpMethod_Put)) + { + StringHttpOutput o1; + HttpOutput o2(o1, false); + RestApiOutput o3(o2, HttpMethod_Put); + RestApiPutCall call(o3, restApi_, RequestOrigin_Documentation, "" /* remote IP */, + "" /* username */, HttpToolbox::Arguments() /* HTTP headers */, + HttpToolbox::Arguments() /* URI components */, + UriComponents() /* trailing */, uri, NULL /* body */, 0 /* body size */); + + bool ok = false; + Json::Value v; + + try + { + ok = (resource.Handle(call) && + call.GetDocumentation().FormatOpenApi(v)); + } + catch (OrthancException&) + { + } + catch (boost::bad_lexical_cast&) + { + } + + if (ok) + { + paths_[path]["put"] = v; + } + else + { + LOG(WARNING) << "Ignoring URI without API documentation: PUT " << path; + } + } + + return true; + } + + + const Json::Value& GetPaths() const + { + return paths_; + } + }; } @@ -162,6 +349,18 @@ + bool RestApi::CreateChunkedRequestReader(std::unique_ptr& target, + RequestOrigin origin, + const char* remoteIp, + const char* username, + HttpMethod method, + const UriComponents& uri, + const HttpToolbox::Arguments& headers) + { + return false; + } + + bool RestApi::Handle(HttpOutput& output, RequestOrigin origin, const char* remoteIp, @@ -255,13 +454,47 @@ } void RestApi::AutoListChildren(RestApiGetCall& call) - { + { + call.GetDocumentation() + .SetTag("Other") + .SetSummary("List of operations") + .SetDescription("List the available operations under URI: " + call.FlattenUri()) + .AddAnswerType(MimeType_Json, "List of the available operations"); + RestApi& context = call.GetContext(); Json::Value directory; if (context.root_.GetDirectory(directory, call.GetFullUri())) { - call.GetOutput().AnswerJson(directory); + if (call.IsDocumentation()) + { + call.GetDocumentation().SetSample(directory); + } + else + { + call.GetOutput().AnswerJson(directory); + } } } + + + void RestApi::GenerateOpenApiDocumentation(Json::Value& target) + { + OpenApiVisitor visitor(*this); + + UriComponents root; + root_.ExploreAllResources(visitor, root); + + target = Json::objectValue; + + target["info"]["version"] = ORTHANC_VERSION; + target["info"]["title"] = "Orthanc"; + + target["openapi"] = "3.0.0"; + + target["servers"].append(Json::objectValue); + target["servers"][0]["url"] = "https://demo.orthanc-server.com/"; + + target["paths"] = visitor.GetPaths(); + } } diff -r 38c22715bb56 -r 80fd140b12ba OrthancFramework/Sources/RestApi/RestApi.h --- a/OrthancFramework/Sources/RestApi/RestApi.h Tue Dec 22 09:39:06 2020 +0100 +++ b/OrthancFramework/Sources/RestApi/RestApi.h Wed Dec 23 12:21:03 2020 +0100 @@ -44,10 +44,7 @@ const char* username, HttpMethod method, const UriComponents& uri, - const HttpToolbox::Arguments& headers) ORTHANC_OVERRIDE - { - return false; - } + const HttpToolbox::Arguments& headers) ORTHANC_OVERRIDE; virtual bool Handle(HttpOutput& output, RequestOrigin origin, @@ -71,5 +68,7 @@ void Register(const std::string& path, RestApiDeleteCall::Handler handler); + + void GenerateOpenApiDocumentation(Json::Value& target); }; } diff -r 38c22715bb56 -r 80fd140b12ba OrthancFramework/Sources/RestApi/RestApiCall.cpp --- a/OrthancFramework/Sources/RestApi/RestApiCall.cpp Tue Dec 22 09:39:06 2020 +0100 +++ b/OrthancFramework/Sources/RestApi/RestApiCall.cpp Wed Dec 23 12:21:03 2020 +0100 @@ -36,4 +36,15 @@ return s; } + + + RestApiCallDocumentation& RestApiCall::GetDocumentation() + { + if (documentation_.get() == NULL) + { + documentation_.reset(new RestApiCallDocumentation(method_)); + } + + return *documentation_; + } } diff -r 38c22715bb56 -r 80fd140b12ba OrthancFramework/Sources/RestApi/RestApiCall.h --- a/OrthancFramework/Sources/RestApi/RestApiCall.h Tue Dec 22 09:39:06 2020 +0100 +++ b/OrthancFramework/Sources/RestApi/RestApiCall.h Wed Dec 23 12:21:03 2020 +0100 @@ -23,6 +23,7 @@ #pragma once #include "../HttpServer/HttpToolbox.h" +#include "RestApiCallDocumentation.h" #include "RestApiPath.h" #include "RestApiOutput.h" @@ -44,6 +45,8 @@ const HttpToolbox::Arguments& uriComponents_; const UriComponents& trailing_; const UriComponents& fullUri_; + HttpMethod method_; // To create RestApiCallDocumentation on demand + std::unique_ptr documentation_; // Lazy creation public: RestApiCall(RestApiOutput& output, @@ -51,6 +54,7 @@ RequestOrigin origin, const char* remoteIp, const char* username, + HttpMethod method, const HttpToolbox::Arguments& httpHeaders, const HttpToolbox::Arguments& uriComponents, const UriComponents& trailing, @@ -63,7 +67,8 @@ httpHeaders_(httpHeaders), uriComponents_(uriComponents), trailing_(trailing), - fullUri_(fullUri) + fullUri_(fullUri), + method_(method) { } @@ -127,5 +132,12 @@ } virtual bool ParseJsonRequest(Json::Value& result) const = 0; + + RestApiCallDocumentation& GetDocumentation(); + + bool IsDocumentation() const + { + return (origin_ == RequestOrigin_Documentation); + } }; } diff -r 38c22715bb56 -r 80fd140b12ba OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp Wed Dec 23 12:21:03 2020 +0100 @@ -0,0 +1,364 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "../PrecompiledHeaders.h" +#include "RestApiCallDocumentation.h" + +#if ORTHANC_ENABLE_CURL == 1 +# include "../HttpClient.h" +#endif + +#include "../Logging.h" +#include "../OrthancException.h" + + +namespace Orthanc +{ + RestApiCallDocumentation& RestApiCallDocumentation::AddRequestType(MimeType mime, + const std::string& description) + { + if (method_ != HttpMethod_Post && + method_ != HttpMethod_Put) + { + throw OrthancException(ErrorCode_BadParameterType, "Request body is only allowed on POST and PUT"); + } + else if (requestTypes_.find(mime) != requestTypes_.end() && + mime != MimeType_Json) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, "Cannot register twice the same type of request: " + + std::string(EnumerationToString(mime))); + } + else + { + requestTypes_[mime] = description; + } + + return *this; + } + + + RestApiCallDocumentation& RestApiCallDocumentation::SetRequestField(const std::string& name, + Type type, + const std::string& description) + { + if (method_ != HttpMethod_Post && + method_ != HttpMethod_Put) + { + throw OrthancException(ErrorCode_BadParameterType, "Request body is only allowed on POST and PUT"); + } + + if (requestTypes_.find(MimeType_Json) == requestTypes_.end()) + { + requestTypes_[MimeType_Json] = ""; + } + + if (requestFields_.find(name) != requestFields_.end()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Field \"" + name + "\" of JSON request is already documented"); + } + else + { + Parameter p; + p.type_ = type; + p.description_ = description; + requestFields_[name] = p; + return *this; + } + } + + + RestApiCallDocumentation& RestApiCallDocumentation::AddAnswerType(MimeType mime, + const std::string& description) + { + if (answerTypes_.find(mime) != answerTypes_.end() && + mime != MimeType_Json) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, "Cannot register twice the same type of answer: " + + std::string(EnumerationToString(mime))); + } + else + { + answerTypes_[mime] = description; + } + + return *this; + } + + + RestApiCallDocumentation& RestApiCallDocumentation::SetUriComponent(const std::string& name, + Type type, + const std::string& description) + { + if (uriComponents_.find(name) != uriComponents_.end()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "URI component \"" + name + "\" is already documented"); + } + else + { + Parameter p; + p.type_ = type; + p.description_ = description; + uriComponents_[name] = p; + return *this; + } + } + + + RestApiCallDocumentation& RestApiCallDocumentation::SetHttpHeader(const std::string& name, + const std::string& description) + { + if (httpHeaders_.find(name) != httpHeaders_.end()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "HTTP header \"" + name + "\" is already documented"); + } + else + { + Parameter p; + p.type_ = Type_String; + p.description_ = description; + httpHeaders_[name] = p; + return *this; + } + } + + + RestApiCallDocumentation& RestApiCallDocumentation::SetHttpGetArgument(const std::string& name, + Type type, + const std::string& description) + { + if (method_ != HttpMethod_Get) + { + throw OrthancException(ErrorCode_InternalError, "Cannot set a HTTP GET argument on HTTP method: " + + std::string(EnumerationToString(method_))); + } + else if (getArguments_.find(name) != getArguments_.end()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "GET argument \"" + name + "\" is already documented"); + } + else + { + Parameter p; + p.type_ = type; + p.description_ = description; + getArguments_[name] = p; + return *this; + } + } + + + RestApiCallDocumentation& RestApiCallDocumentation::SetAnswerField(const std::string& name, + Type type, + const std::string& description) + { + if (answerTypes_.find(MimeType_Json) == answerTypes_.end()) + { + answerTypes_[MimeType_Json] = ""; + } + + if (answerFields_.find(name) != answerFields_.end()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Field \"" + name + "\" of JSON answer is already documented"); + } + else + { + Parameter p; + p.type_ = type; + p.description_ = description; + answerFields_[name] = p; + return *this; + } + } + + + void RestApiCallDocumentation::SetHttpGetSample(const std::string& url) + { +#if ORTHANC_ENABLE_CURL == 1 + HttpClient client; + client.SetUrl(url); + client.SetHttpsVerifyPeers(false); + if (!client.Apply(sample_)) + { + LOG(ERROR) << "Cannot GET: " << url; + sample_ = Json::nullValue; + } +#else + LOG(WARNING) << "HTTP client is not available to generated the documentation"; +#endif + } + + + static const char* TypeToString(RestApiCallDocumentation::Type type) + { + switch (type) + { + case RestApiCallDocumentation::Type_Unknown: + throw OrthancException(ErrorCode_ParameterOutOfRange); + + case RestApiCallDocumentation::Type_String: + case RestApiCallDocumentation::Type_Text: + return "string"; + + case RestApiCallDocumentation::Type_Number: + return "number"; + + case RestApiCallDocumentation::Type_Boolean: + return "boolean"; + + case RestApiCallDocumentation::Type_JsonObject: + case RestApiCallDocumentation::Type_JsonListOfStrings: + return "object"; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + bool RestApiCallDocumentation::FormatOpenApi(Json::Value& target) const + { + if (summary_.empty() && + description_.empty()) + { + return false; + } + else + { + target = Json::objectValue; + + if (!tag_.empty()) + { + target["tags"].append(tag_); + } + + if (!summary_.empty()) + { + target["summary"] = summary_; + } + else if (!description_.empty()) + { + target["summary"] = description_; + } + + if (!description_.empty()) + { + target["description"] = description_; + } + else if (!summary_.empty()) + { + target["description"] = summary_; + } + + if (method_ == HttpMethod_Post || + method_ == HttpMethod_Put) + { + for (AllowedTypes::const_iterator it = requestTypes_.begin(); + it != requestTypes_.end(); ++it) + { + Json::Value& schema = target["requestBody"]["content"][EnumerationToString(it->first)]["schema"]; + schema["description"] = it->second; + + if (it->first == MimeType_Json) + { + for (Parameters::const_iterator it = requestFields_.begin(); + it != requestFields_.end(); ++it) + { + Json::Value p = Json::objectValue; + p["type"] = TypeToString(it->second.type_); + p["description"] = it->second.description_; + schema["properties"][it->first] = p; + } + } + } + } + + target["responses"]["200"]["description"] = (answerDescription_.empty() ? "" : answerDescription_); + + for (AllowedTypes::const_iterator it = answerTypes_.begin(); + it != answerTypes_.end(); ++it) + { + Json::Value& schema = target["responses"]["200"]["content"][EnumerationToString(it->first)]["schema"]; + schema["description"] = it->second; + + if (it->first == MimeType_Json) + { + for (Parameters::const_iterator it = answerFields_.begin(); + it != answerFields_.end(); ++it) + { + Json::Value p = Json::objectValue; + p["type"] = TypeToString(it->second.type_); + p["description"] = it->second.description_; + schema["properties"][it->first] = p; + } + } + } + + if (sample_.type() != Json::nullValue) + { + target["responses"]["200"]["content"]["application/json"]["schema"]["example"] = sample_; + } + else + { + target["responses"]["200"]["content"]["application/json"]["examples"] = Json::arrayValue; + } + + Json::Value parameters = Json::arrayValue; + + for (Parameters::const_iterator it = getArguments_.begin(); + it != getArguments_.end(); ++it) + { + Json::Value p = Json::objectValue; + p["name"] = it->first; + p["in"] = "query"; + p["schema"]["type"] = TypeToString(it->second.type_); + p["description"] = it->second.description_; + parameters.append(p); + } + + for (Parameters::const_iterator it = httpHeaders_.begin(); + it != httpHeaders_.end(); ++it) + { + Json::Value p = Json::objectValue; + p["name"] = it->first; + p["in"] = "header"; + p["schema"]["type"] = TypeToString(it->second.type_); + p["description"] = it->second.description_; + parameters.append(p); + } + + for (Parameters::const_iterator it = uriComponents_.begin(); + it != uriComponents_.end(); ++it) + { + Json::Value p = Json::objectValue; + p["name"] = it->first; + p["in"] = "path"; + p["required"] = true; + p["schema"]["type"] = TypeToString(it->second.type_); + p["description"] = it->second.description_; + parameters.append(p); + } + + target["parameters"] = parameters; + + return true; + } + } +} diff -r 38c22715bb56 -r 80fd140b12ba OrthancFramework/Sources/RestApi/RestApiCallDocumentation.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/RestApi/RestApiCallDocumentation.h Wed Dec 23 12:21:03 2020 +0100 @@ -0,0 +1,132 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include "../Enumerations.h" + +#include +#include + +#include +#include + +namespace Orthanc +{ + class RestApiCallDocumentation : public boost::noncopyable + { + public: + enum Type + { + Type_Unknown, + Type_Text, + Type_String, + Type_Number, + Type_Boolean, + Type_JsonListOfStrings, + Type_JsonObject + }; + + private: + struct Parameter + { + Type type_; + std::string description_; + }; + + typedef std::map Parameters; + typedef std::map AllowedTypes; + + HttpMethod method_; + std::string tag_; + std::string summary_; + std::string description_; + Parameters uriComponents_; + Parameters httpHeaders_; + Parameters getArguments_; + AllowedTypes requestTypes_; + Parameters requestFields_; // For JSON request + AllowedTypes answerTypes_; + Parameters answerFields_; // Only if JSON object + std::string answerDescription_; + Json::Value sample_; + + public: + RestApiCallDocumentation(HttpMethod method) : + method_(method), + sample_(Json::nullValue) + { + } + + RestApiCallDocumentation& SetTag(const std::string& tag) + { + tag_ = tag; + return *this; + } + + RestApiCallDocumentation& SetSummary(const std::string& summary) + { + summary_ = summary; + return *this; + } + + RestApiCallDocumentation& SetDescription(const std::string& description) + { + description_ = description; + return *this; + } + + RestApiCallDocumentation& AddRequestType(MimeType mime, + const std::string& description); + + RestApiCallDocumentation& SetRequestField(const std::string& name, + Type type, + const std::string& description); + + RestApiCallDocumentation& AddAnswerType(MimeType type, + const std::string& description); + + RestApiCallDocumentation& SetUriComponent(const std::string& name, + Type type, + const std::string& description); + + RestApiCallDocumentation& SetHttpHeader(const std::string& name, + const std::string& description); + + RestApiCallDocumentation& SetHttpGetArgument(const std::string& name, + Type type, + const std::string& description); + + RestApiCallDocumentation& SetAnswerField(const std::string& name, + Type type, + const std::string& description); + + void SetHttpGetSample(const std::string& url); + + void SetSample(const Json::Value& sample) + { + sample_ = sample; + } + + bool FormatOpenApi(Json::Value& target) const; + }; +} diff -r 38c22715bb56 -r 80fd140b12ba OrthancFramework/Sources/RestApi/RestApiDeleteCall.h --- a/OrthancFramework/Sources/RestApi/RestApiDeleteCall.h Tue Dec 22 09:39:06 2020 +0100 +++ b/OrthancFramework/Sources/RestApi/RestApiDeleteCall.h Wed Dec 23 12:21:03 2020 +0100 @@ -40,7 +40,7 @@ const HttpToolbox::Arguments& uriComponents, const UriComponents& trailing, const UriComponents& fullUri) : - RestApiCall(output, context, origin, remoteIp, username, + RestApiCall(output, context, origin, remoteIp, username, HttpMethod_Delete, httpHeaders, uriComponents, trailing, fullUri) { } diff -r 38c22715bb56 -r 80fd140b12ba OrthancFramework/Sources/RestApi/RestApiGetCall.h --- a/OrthancFramework/Sources/RestApi/RestApiGetCall.h Tue Dec 22 09:39:06 2020 +0100 +++ b/OrthancFramework/Sources/RestApi/RestApiGetCall.h Wed Dec 23 12:21:03 2020 +0100 @@ -44,7 +44,7 @@ const UriComponents& trailing, const UriComponents& fullUri, const HttpToolbox::Arguments& getArguments) : - RestApiCall(output, context, origin, remoteIp, username, + RestApiCall(output, context, origin, remoteIp, username, HttpMethod_Get, httpHeaders, uriComponents, trailing, fullUri), getArguments_(getArguments) { diff -r 38c22715bb56 -r 80fd140b12ba OrthancFramework/Sources/RestApi/RestApiHierarchy.cpp --- a/OrthancFramework/Sources/RestApi/RestApiHierarchy.cpp Tue Dec 22 09:39:06 2020 +0100 +++ b/OrthancFramework/Sources/RestApi/RestApiHierarchy.cpp Wed Dec 23 12:21:03 2020 +0100 @@ -39,7 +39,7 @@ } - bool RestApiHierarchy::Resource::HasHandler(HttpMethod method) const + bool RestApiHierarchy::Resource::HasMethod(HttpMethod method) const { switch (method) { @@ -186,7 +186,7 @@ { if (path.IsUniversalTrailing()) { - universalHandlers_.Register(handler); + handlersWithTrailing_.Register(handler); } else { @@ -221,14 +221,14 @@ return false; } - UriComponents trailing; - // Look for an exact match on the resource of interest if (uri.size() == 0 || level == uri.size()) { + UriComponents noTrailing; + if (!handlers_.IsEmpty() && - visitor.Visit(handlers_, uri, components, trailing)) + visitor.Visit(handlers_, uri, false, components, noTrailing)) { return true; } @@ -263,8 +263,9 @@ // As a last resort, call the universal handlers, if any - if (!universalHandlers_.IsEmpty()) + if (!handlersWithTrailing_.IsEmpty()) { + UriComponents trailing; trailing.resize(uri.size() - level); size_t pos = 0; for (size_t i = level; i < uri.size(); i++, pos++) @@ -274,7 +275,7 @@ assert(pos == trailing.size()); - if (visitor.Visit(universalHandlers_, uri, components, trailing)) + if (visitor.Visit(handlersWithTrailing_, uri, true, components, trailing)) { return true; } @@ -286,7 +287,7 @@ bool RestApiHierarchy::CanGenerateDirectory() const { - return (universalHandlers_.IsEmpty() && + return (handlersWithTrailing_.IsEmpty() && wildcardChildren_.empty()); } @@ -376,22 +377,22 @@ target = Json::objectValue; /*std::string s = " "; - if (handlers_.HasHandler(HttpMethod_Get)) + if (handlers_.HasMethod(HttpMethod_Get)) { s += "GET "; } - if (handlers_.HasHandler(HttpMethod_Post)) + if (handlers_.HasMethod(HttpMethod_Post)) { s += "POST "; } - if (handlers_.HasHandler(HttpMethod_Put)) + if (handlers_.HasMethod(HttpMethod_Put)) { s += "PUT "; } - if (handlers_.HasHandler(HttpMethod_Delete)) + if (handlers_.HasMethod(HttpMethod_Delete)) { s += "DELETE "; } @@ -443,27 +444,28 @@ virtual bool Visit(const RestApiHierarchy::Resource& resource, const UriComponents& uri, + bool hasTrailing, const HttpToolbox::Arguments& components, const UriComponents& trailing) { - if (trailing.size() == 0) // Ignore universal handlers + if (!hasTrailing) // Ignore universal handlers { - if (resource.HasHandler(HttpMethod_Get)) + if (resource.HasMethod(HttpMethod_Get)) { methods_.insert(HttpMethod_Get); } - if (resource.HasHandler(HttpMethod_Post)) + if (resource.HasMethod(HttpMethod_Post)) { methods_.insert(HttpMethod_Post); } - if (resource.HasHandler(HttpMethod_Put)) + if (resource.HasMethod(HttpMethod_Put)) { methods_.insert(HttpMethod_Put); } - if (resource.HasHandler(HttpMethod_Delete)) + if (resource.HasMethod(HttpMethod_Delete)) { methods_.insert(HttpMethod_Delete); } @@ -489,4 +491,35 @@ } } + void RestApiHierarchy::ExploreAllResources(IVisitor& visitor, + const UriComponents& path) const + { + if (!handlers_.IsEmpty()) + { + visitor.Visit(handlers_, path, false, HttpToolbox::Arguments(), UriComponents()); + } + + if (!handlersWithTrailing_.IsEmpty()) + { + visitor.Visit(handlersWithTrailing_, path, true, HttpToolbox::Arguments(), UriComponents()); + } + + for (Children::const_iterator + it = children_.begin(); it != children_.end(); ++it) + { + assert(it->second != NULL); + UriComponents c = path; + c.push_back(it->first); + it->second->ExploreAllResources(visitor, c); + } + + for (Children::const_iterator + it = wildcardChildren_.begin(); it != wildcardChildren_.end(); ++it) + { + assert(it->second != NULL); + UriComponents c = path; + c.push_back("{" + it->first + "}"); + it->second->ExploreAllResources(visitor, c); + } + } } diff -r 38c22715bb56 -r 80fd140b12ba OrthancFramework/Sources/RestApi/RestApiHierarchy.h --- a/OrthancFramework/Sources/RestApi/RestApiHierarchy.h Tue Dec 22 09:39:06 2020 +0100 +++ b/OrthancFramework/Sources/RestApi/RestApiHierarchy.h Wed Dec 23 12:21:03 2020 +0100 @@ -45,7 +45,7 @@ public: Resource(); - bool HasHandler(HttpMethod method) const; + bool HasMethod(HttpMethod method) const; void Register(RestApiGetCall::Handler handler); @@ -76,7 +76,9 @@ virtual bool Visit(const Resource& resource, const UriComponents& uri, - const HttpToolbox::Arguments& components, + bool hasTrailing, + // The two arguments below are empty if using "ExploreAllResources()" + const HttpToolbox::Arguments& uriComponents, const UriComponents& trailing) = 0; }; @@ -87,7 +89,7 @@ Resource handlers_; Children children_; Children wildcardChildren_; - Resource universalHandlers_; + Resource handlersWithTrailing_; static RestApiHierarchy& AddChild(Children& children, const std::string& name); @@ -135,5 +137,8 @@ void GetAcceptedMethods(std::set& methods, const UriComponents& uri); + + void ExploreAllResources(IVisitor& visitor, + const UriComponents& path) const; }; } diff -r 38c22715bb56 -r 80fd140b12ba OrthancFramework/Sources/RestApi/RestApiOutput.h --- a/OrthancFramework/Sources/RestApi/RestApiOutput.h Tue Dec 22 09:39:06 2020 +0100 +++ b/OrthancFramework/Sources/RestApi/RestApiOutput.h Wed Dec 23 12:21:03 2020 +0100 @@ -32,10 +32,10 @@ class RestApiOutput { private: - HttpOutput& output_; - HttpMethod method_; - bool alreadySent_; - bool convertJsonToXml_; + HttpOutput& output_; + HttpMethod method_; + bool alreadySent_; + bool convertJsonToXml_; void CheckStatus(); diff -r 38c22715bb56 -r 80fd140b12ba OrthancFramework/Sources/RestApi/RestApiPostCall.h --- a/OrthancFramework/Sources/RestApi/RestApiPostCall.h Tue Dec 22 09:39:06 2020 +0100 +++ b/OrthancFramework/Sources/RestApi/RestApiPostCall.h Wed Dec 23 12:21:03 2020 +0100 @@ -46,7 +46,7 @@ const UriComponents& fullUri, const void* bodyData, size_t bodySize) : - RestApiCall(output, context, origin, remoteIp, username, + RestApiCall(output, context, origin, remoteIp, username, HttpMethod_Post, httpHeaders, uriComponents, trailing, fullUri), bodyData_(bodyData), bodySize_(bodySize) diff -r 38c22715bb56 -r 80fd140b12ba OrthancFramework/Sources/RestApi/RestApiPutCall.h --- a/OrthancFramework/Sources/RestApi/RestApiPutCall.h Tue Dec 22 09:39:06 2020 +0100 +++ b/OrthancFramework/Sources/RestApi/RestApiPutCall.h Wed Dec 23 12:21:03 2020 +0100 @@ -46,7 +46,7 @@ const UriComponents& fullUri, const void* bodyData, size_t bodySize) : - RestApiCall(output, context, origin, remoteIp, username, + RestApiCall(output, context, origin, remoteIp, username, HttpMethod_Put, httpHeaders, uriComponents, trailing, fullUri), bodyData_(bodyData), bodySize_(bodySize) diff -r 38c22715bb56 -r 80fd140b12ba OrthancFramework/Sources/SystemToolbox.cpp --- a/OrthancFramework/Sources/SystemToolbox.cpp Tue Dec 22 09:39:06 2020 +0100 +++ b/OrthancFramework/Sources/SystemToolbox.cpp Wed Dec 23 12:21:03 2020 +0100 @@ -213,30 +213,41 @@ log); } - boost::filesystem::ifstream f; - f.open(path, std::ifstream::in | std::ifstream::binary); - if (!f.good()) + try { - throw OrthancException(ErrorCode_InexistentFile, - "File not found: " + path, - log); - } + boost::filesystem::ifstream f; + f.open(path, std::ifstream::in | std::ifstream::binary); + if (!f.good()) + { + throw OrthancException(ErrorCode_InexistentFile, + "File not found: " + path, + log); + } - std::streamsize size = GetStreamSize(f); - content.resize(static_cast(size)); + std::streamsize size = GetStreamSize(f); + content.resize(static_cast(size)); - if (static_cast(content.size()) != size) - { - throw OrthancException(ErrorCode_InternalError, - "Reading a file that is too large for a 32bit architecture"); - } + if (static_cast(content.size()) != size) + { + throw OrthancException(ErrorCode_InternalError, + "Reading a file that is too large for a 32bit architecture"); + } - if (size != 0) + if (size != 0) + { + f.read(&content[0], size); + } + + f.close(); + } + catch (boost::filesystem::filesystem_error&) { - f.read(&content[0], size); + throw OrthancException(ErrorCode_InexistentFile); } - - f.close(); + catch (std::system_error&) + { + throw OrthancException(ErrorCode_InexistentFile); + } } @@ -256,38 +267,49 @@ "The path does not point to a regular file: " + path); } - boost::filesystem::ifstream f; - f.open(path, std::ifstream::in | std::ifstream::binary); - if (!f.good()) + try + { + boost::filesystem::ifstream f; + f.open(path, std::ifstream::in | std::ifstream::binary); + if (!f.good()) + { + throw OrthancException(ErrorCode_InexistentFile); + } + + bool full = true; + + { + std::streamsize size = GetStreamSize(f); + if (size <= 0) + { + headerSize = 0; + full = false; + } + else if (static_cast(size) < headerSize) + { + headerSize = static_cast(size); // Truncate to the size of the file + full = false; + } + } + + header.resize(headerSize); + if (headerSize != 0) + { + f.read(&header[0], headerSize); + } + + f.close(); + + return full; + } + catch (boost::filesystem::filesystem_error&) { throw OrthancException(ErrorCode_InexistentFile); } - - bool full = true; - + catch (std::system_error&) { - std::streamsize size = GetStreamSize(f); - if (size <= 0) - { - headerSize = 0; - full = false; - } - else if (static_cast(size) < headerSize) - { - headerSize = static_cast(size); // Truncate to the size of the file - full = false; - } + throw OrthancException(ErrorCode_InexistentFile); } - - header.resize(headerSize); - if (headerSize != 0) - { - f.read(&header[0], headerSize); - } - - f.close(); - - return full; } @@ -296,56 +318,67 @@ const std::string& path, bool callFsync) { - //boost::filesystem::ofstream f; - boost::iostreams::stream f; + try + { + //boost::filesystem::ofstream f; + boost::iostreams::stream f; - f.open(path, std::ofstream::out | std::ofstream::binary); - if (!f.good()) + f.open(path, std::ofstream::out | std::ofstream::binary); + if (!f.good()) + { + throw OrthancException(ErrorCode_CannotWriteFile); + } + + if (size != 0) + { + f.write(reinterpret_cast(content), size); + + if (!f.good()) + { + f.close(); + throw OrthancException(ErrorCode_CannotWriteFile); + } + } + + if (callFsync) + { + // https://stackoverflow.com/a/23826489/881731 + f.flush(); + + bool success; + + /** + * "f->handle()" corresponds to "FILE*" (aka "HANDLE") on + * Microsoft Windows, and to "int" (file descriptor) on other + * systems: + * https://github.com/boostorg/iostreams/blob/develop/include/boost/iostreams/detail/file_handle.hpp + **/ + +#if defined(_WIN32) + // https://docs.microsoft.com/fr-fr/windows/win32/api/fileapi/nf-fileapi-flushfilebuffers + success = (::FlushFileBuffers(f->handle()) != 0); +#elif (_POSIX_C_SOURCE >= 199309L || _XOPEN_SOURCE >= 500) + success = (::fdatasync(f->handle()) == 0); +#else + success = (::fsync(f->handle()) == 0); +#endif + + if (!success) + { + throw OrthancException(ErrorCode_CannotWriteFile, "Cannot force flush to disk"); + } + } + + f.close(); + } + catch (boost::filesystem::filesystem_error&) { throw OrthancException(ErrorCode_CannotWriteFile); } - - if (size != 0) - { - f.write(reinterpret_cast(content), size); - - if (!f.good()) - { - f.close(); - throw OrthancException(ErrorCode_FileStorageCannotWrite); - } - } - - if (callFsync) + catch (std::system_error&) { - // https://stackoverflow.com/a/23826489/881731 - f.flush(); - - bool success; - - /** - * "f->handle()" corresponds to "FILE*" (aka "HANDLE") on - * Microsoft Windows, and to "int" (file descriptor) on other - * systems: - * https://github.com/boostorg/iostreams/blob/develop/include/boost/iostreams/detail/file_handle.hpp - **/ - -#if defined(_WIN32) - // https://docs.microsoft.com/fr-fr/windows/win32/api/fileapi/nf-fileapi-flushfilebuffers - success = (::FlushFileBuffers(f->handle()) != 0); -#elif (_POSIX_C_SOURCE >= 199309L || _XOPEN_SOURCE >= 500) - success = (::fdatasync(f->handle()) == 0); -#else - success = (::fsync(f->handle()) == 0); -#endif - - if (!success) - { - throw OrthancException(ErrorCode_FileStorageCannotWrite, "Cannot force flush to disk"); - } + throw OrthancException(ErrorCode_CannotWriteFile); } - - f.close(); } @@ -396,6 +429,10 @@ { throw OrthancException(ErrorCode_InexistentFile); } + catch (std::system_error&) + { + throw OrthancException(ErrorCode_InexistentFile); + } } @@ -575,18 +612,16 @@ bool SystemToolbox::IsRegularFile(const std::string& path) { - namespace fs = boost::filesystem; - try { - if (fs::exists(path)) + if (boost::filesystem::exists(path)) { - fs::file_status status = fs::status(path); + boost::filesystem::file_status status = boost::filesystem::status(path); return (status.type() == boost::filesystem::regular_file || status.type() == boost::filesystem::reparse_file); // Fix BitBucket issue #11 } } - catch (fs::filesystem_error&) + catch (boost::filesystem::filesystem_error&) { } diff -r 38c22715bb56 -r 80fd140b12ba OrthancFramework/UnitTestsSources/FrameworkTests.cpp --- a/OrthancFramework/UnitTestsSources/FrameworkTests.cpp Tue Dec 22 09:39:06 2020 +0100 +++ b/OrthancFramework/UnitTestsSources/FrameworkTests.cpp Wed Dec 23 12:21:03 2020 +0100 @@ -787,6 +787,23 @@ ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Study)); ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Series)); ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Instance)); + + ASSERT_STREQ("Patients", GetResourceTypeText(ResourceType_Patient, true /* plural */, true /* upper case */)); + ASSERT_STREQ("patients", GetResourceTypeText(ResourceType_Patient, true, false)); + ASSERT_STREQ("Patient", GetResourceTypeText(ResourceType_Patient, false, true)); + ASSERT_STREQ("patient", GetResourceTypeText(ResourceType_Patient, false, false)); + ASSERT_STREQ("Studies", GetResourceTypeText(ResourceType_Study, true, true)); + ASSERT_STREQ("studies", GetResourceTypeText(ResourceType_Study, true, false)); + ASSERT_STREQ("Study", GetResourceTypeText(ResourceType_Study, false, true)); + ASSERT_STREQ("study", GetResourceTypeText(ResourceType_Study, false, false)); + ASSERT_STREQ("Series", GetResourceTypeText(ResourceType_Series, true, true)); + ASSERT_STREQ("series", GetResourceTypeText(ResourceType_Series, true, false)); + ASSERT_STREQ("Series", GetResourceTypeText(ResourceType_Series, false, true)); + ASSERT_STREQ("series", GetResourceTypeText(ResourceType_Series, false, false)); + ASSERT_STREQ("Instances", GetResourceTypeText(ResourceType_Instance, true, true)); + ASSERT_STREQ("instances", GetResourceTypeText(ResourceType_Instance, true, false)); + ASSERT_STREQ("Instance", GetResourceTypeText(ResourceType_Instance, false, true)); + ASSERT_STREQ("instance", GetResourceTypeText(ResourceType_Instance, false, false)); } diff -r 38c22715bb56 -r 80fd140b12ba OrthancFramework/UnitTestsSources/RestApiTests.cpp --- a/OrthancFramework/UnitTestsSources/RestApiTests.cpp Tue Dec 22 09:39:06 2020 +0100 +++ b/OrthancFramework/UnitTestsSources/RestApiTests.cpp Wed Dec 23 12:21:03 2020 +0100 @@ -312,6 +312,7 @@ public: virtual bool Visit(const RestApiHierarchy::Resource& resource, const UriComponents& uri, + bool hasTrailing, const HttpToolbox::Arguments& components, const UriComponents& trailing) ORTHANC_OVERRIDE { diff -r 38c22715bb56 -r 80fd140b12ba OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp Tue Dec 22 09:39:06 2020 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp Wed Dec 23 12:21:03 2020 +0100 @@ -120,6 +120,27 @@ static void UploadDicomFile(RestApiPostCall& call) { + if (call.GetRequestOrigin() == RequestOrigin_Documentation) + { + Json::Value sample = Json::objectValue; + sample["ID"] = "19816330-cb02e1cf-df3a8fe8-bf510623-ccefe9f5"; + sample["ParentPatient"] = "ef9d77db-eb3b2bef-9b31fd3e-bf42ae46-dbdb0cc3"; + sample["ParentSeries"] = "3774320f-ccda46d8-69ee8641-9e791cbf-3ecbbcc6"; + sample["ParentStudy"] = "66c8e41e-ac3a9029-0b85e42a-8195ee0a-92c2e62e"; + sample["Path"] = "/instances/19816330-cb02e1cf-df3a8fe8-bf510623-ccefe9f5"; + sample["Status"] = "Success"; + + call.GetDocumentation() + .SetTag("Instances") + .SetSummary("Upload DICOM files") + .AddRequestType(MimeType_Dicom, "DICOM file to be uploaded") + .AddRequestType(MimeType_Zip, "ZIP archive containing DICOM files (new in Orthanc 1.8.2)") + .AddAnswerType(MimeType_Json, "Information about the uploaded instance, " + "or list of information for each uploaded instance in the case of ZIP archive") + .SetSample(sample); + return; + } + ServerContext& context = OrthancRestApi::GetContext(call); CLOG(INFO, HTTP) << "Receiving a DICOM file of " << call.GetBodySize() << " bytes through HTTP"; diff -r 38c22715bb56 -r 80fd140b12ba OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Tue Dec 22 09:39:06 2020 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Wed Dec 23 12:21:03 2020 +0100 @@ -161,6 +161,21 @@ template static void ListResources(RestApiGetCall& call) { + if (call.IsDocumentation()) + { + const std::string resources = GetResourceTypeText(resourceType, true /* plural */, false /* lower case */); + call.GetDocumentation() + .SetTag(GetResourceTypeText(resourceType, true /* plural */, true /* upper case */)) + .SetSummary("List the available " + resources) + .SetDescription("List the Orthanc identifiers of all the available DICOM " + resources) + .SetHttpGetArgument("limit", RestApiCallDocumentation::Type_Number, "Limit the number of results") + .SetHttpGetArgument("since", RestApiCallDocumentation::Type_Number, "Show only the resources since the provided index") + .SetHttpGetArgument("expand", RestApiCallDocumentation::Type_String, + "If present, retrieve detailed information about the individual " + resources) + .SetHttpGetSample("https://demo.orthanc-server.com/" + resources + "?since=0&limit=2"); + return; + } + ServerIndex& index = OrthancRestApi::GetIndex(call); std::list result; @@ -198,6 +213,37 @@ template static void GetSingleResource(RestApiGetCall& call) { + if (call.IsDocumentation()) + { + std::string sampleUrl; + switch (resourceType) + { + case Orthanc::ResourceType_Instance: + sampleUrl = "https://demo.orthanc-server.com/instances/d94d9a03-3003b047-a4affc69-322313b2-680530a2"; + break; + case Orthanc::ResourceType_Series: + sampleUrl = "https://demo.orthanc-server.com/series/37836232-d13a2350-fa1dedc5-962b31aa-010f8e52"; + break; + case Orthanc::ResourceType_Study: + sampleUrl = "https://demo.orthanc-server.com/studies/27f7126f-4f66fb14-03f4081b-f9341db2-53925988"; + break; + case Orthanc::ResourceType_Patient: + sampleUrl = "https://demo.orthanc-server.com/patients/46e6332c-677825b6-202fcf7c-f787bc5f-7b07c382"; + break; + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + const std::string resource = GetResourceTypeText(resourceType, false /* plural */, false /* lower case */); + call.GetDocumentation() + .SetTag(GetResourceTypeText(resourceType, true /* plural */, true /* upper case */)) + .SetSummary("Get information about some " + resource) + .SetDescription("Get information about the DICOM " + resource + " of interest whose Orthanc identifier is provided in the URL") + .SetUriComponent("id", RestApiCallDocumentation::Type_String, "Orthanc identifier of the " + resource + " of interest") + .SetHttpGetSample(sampleUrl); + return; + } + Json::Value result; if (OrthancRestApi::GetIndex(call).LookupResource(result, call.GetUriComponent("id", ""), resourceType)) { @@ -208,6 +254,17 @@ template static void DeleteSingleResource(RestApiDeleteCall& call) { + if (call.IsDocumentation()) + { + const std::string resource = GetResourceTypeText(resourceType, false /* plural */, false /* lower case */); + call.GetDocumentation() + .SetTag(GetResourceTypeText(resourceType, true /* plural */, true /* upper case */)) + .SetSummary("Delete some " + resource) + .SetDescription("Delete the DICOM " + resource + " of interest whose Orthanc identifier is provided in the URL") + .SetUriComponent("id", RestApiCallDocumentation::Type_String, "Orthanc identifier of the " + resource + " of interest"); + return; + } + Json::Value result; if (OrthancRestApi::GetContext(call).DeleteResource(result, call.GetUriComponent("id", ""), resourceType)) { @@ -220,6 +277,16 @@ static void IsProtectedPatient(RestApiGetCall& call) { + if (call.IsDocumentation()) + { + call.GetDocumentation() + .SetTag("Patients") + .SetSummary("Is the patient protected against recycling?") + .SetUriComponent("id", RestApiCallDocumentation::Type_String, "Orthanc identifier of the patient of interest") + .AddAnswerType(MimeType_PlainText, "\"1\" if protected, \"0\" if not protected"); + return; + } + std::string publicId = call.GetUriComponent("id", ""); bool isProtected = OrthancRestApi::GetIndex(call).IsProtectedPatient(publicId); call.GetOutput().AnswerBuffer(isProtected ? "1" : "0", MimeType_PlainText); @@ -228,6 +295,16 @@ static void SetPatientProtection(RestApiPutCall& call) { + if (call.IsDocumentation()) + { + call.GetDocumentation() + .SetTag("Patients") + .SetSummary("Protect one patient against recycling") + .SetDescription("Check out configuration options \"MaximumStorageSize\" and \"MaximumPatientCount\"") + .SetUriComponent("id", RestApiCallDocumentation::Type_String, "Orthanc identifier of the patient of interest"); + return; + } + ServerContext& context = OrthancRestApi::GetContext(call); std::string publicId = call.GetUriComponent("id", ""); diff -r 38c22715bb56 -r 80fd140b12ba OrthancServer/Sources/main.cpp --- a/OrthancServer/Sources/main.cpp Tue Dec 22 09:39:06 2020 +0100 +++ b/OrthancServer/Sources/main.cpp Wed Dec 23 12:21:03 2020 +0100 @@ -39,11 +39,13 @@ #include "../../OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.h" #include "../../OrthancFramework/Sources/DicomNetworking/DicomServer.h" #include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" +#include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h" #include "../../OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.h" #include "../../OrthancFramework/Sources/HttpServer/HttpServer.h" #include "../../OrthancFramework/Sources/Logging.h" #include "../../OrthancFramework/Sources/Lua/LuaFunctionCall.h" #include "../Plugins/Engine/OrthancPlugins.h" +#include "Database/SQLiteDatabaseWrapper.h" #include "EmbeddedResourceHttpHandler.h" #include "OrthancConfiguration.h" #include "OrthancFindRequestHandler.h" @@ -655,8 +657,10 @@ << " --upgrade\t\tallow Orthanc to upgrade the version of the" << std::endl << "\t\t\tdatabase (beware that the database will become" << std::endl << "\t\t\tincompatible with former versions of Orthanc)" << std::endl - << " --no-jobs\t\tDon't restart the jobs that were stored during" << std::endl + << " --no-jobs\t\tdon't restart the jobs that were stored during" << std::endl << "\t\t\tthe last execution of Orthanc" << std::endl + << " --openapi=[file]\twrite the OpenAPI documentation and exit" << std::endl + << "\t\t\t(if \"file\" is \"-\", dumps to stdout)" << std::endl << " --version\t\toutput version information and exit" << std::endl << std::endl << "Fine-tuning of log categories:" << std::endl; @@ -1745,6 +1749,43 @@ return -1; } } + else if (boost::starts_with(argument, "--openapi=")) + { + std::string target = argument.substr(10); + + try + { + Json::Value openapi; + + { + SQLiteDatabaseWrapper inMemoryDatabase; + inMemoryDatabase.Open(); + MemoryStorageArea inMemoryStorage; + ServerContext context(inMemoryDatabase, inMemoryStorage, true /* unit testing */, 0 /* max completed jobs */); + OrthancRestApi restApi(context, false /* no Orthanc Explorer */); + restApi.GenerateOpenApiDocumentation(openapi); + context.Stop(); + } + + std::string s; + Toolbox::WriteStyledJson(s, openapi); + + if (target == "-") + { + std::cout << s; // Print to stdout + } + else + { + SystemToolbox::WriteFile(s, target); + } + return 0; + } + catch (OrthancException&) + { + LOG(ERROR) << "Cannot export OpenAPI documentation as file \"" << target << "\""; + return -1; + } + } else { LOG(WARNING) << "Option unsupported by the core of Orthanc: " << argument;