summaryrefslogtreecommitdiffstats
path: root/xbmc/interfaces/legacy/wsgi
diff options
context:
space:
mode:
Diffstat (limited to 'xbmc/interfaces/legacy/wsgi')
-rw-r--r--xbmc/interfaces/legacy/wsgi/CMakeLists.txt13
-rw-r--r--xbmc/interfaces/legacy/wsgi/WsgiErrorStream.cpp63
-rw-r--r--xbmc/interfaces/legacy/wsgi/WsgiErrorStream.h92
-rw-r--r--xbmc/interfaces/legacy/wsgi/WsgiInputStream.cpp166
-rw-r--r--xbmc/interfaces/legacy/wsgi/WsgiInputStream.h120
-rw-r--r--xbmc/interfaces/legacy/wsgi/WsgiResponse.cpp92
-rw-r--r--xbmc/interfaces/legacy/wsgi/WsgiResponse.h77
-rw-r--r--xbmc/interfaces/legacy/wsgi/WsgiResponseBody.cpp29
-rw-r--r--xbmc/interfaces/legacy/wsgi/WsgiResponseBody.h50
9 files changed, 702 insertions, 0 deletions
diff --git a/xbmc/interfaces/legacy/wsgi/CMakeLists.txt b/xbmc/interfaces/legacy/wsgi/CMakeLists.txt
new file mode 100644
index 0000000..cc29eb4
--- /dev/null
+++ b/xbmc/interfaces/legacy/wsgi/CMakeLists.txt
@@ -0,0 +1,13 @@
+if(MICROHTTPD_FOUND)
+ set(SOURCES WsgiErrorStream.cpp
+ WsgiInputStream.cpp
+ WsgiResponseBody.cpp
+ WsgiResponse.cpp)
+
+ set(HEADERS WsgiErrorStream.h
+ WsgiInputStream.h
+ WsgiResponse.h
+ WsgiResponseBody.h)
+
+ core_add_library(legacy_interface_wsgi)
+endif()
diff --git a/xbmc/interfaces/legacy/wsgi/WsgiErrorStream.cpp b/xbmc/interfaces/legacy/wsgi/WsgiErrorStream.cpp
new file mode 100644
index 0000000..b8096fe
--- /dev/null
+++ b/xbmc/interfaces/legacy/wsgi/WsgiErrorStream.cpp
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2015-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "WsgiErrorStream.h"
+
+#include "network/httprequesthandler/python/HTTPPythonRequest.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+namespace XBMCAddon
+{
+ namespace xbmcwsgi
+ {
+ WsgiErrorStream::WsgiErrorStream()
+ : m_request(NULL)
+ { }
+
+ WsgiErrorStream::~WsgiErrorStream()
+ {
+ m_request = NULL;
+ }
+
+ void WsgiErrorStream::write(const String& str)
+ {
+ if (str.empty())
+ return;
+
+ String msg = str;
+ // remove a trailing \n
+ if (msg.at(msg.size() - 1) == '\n')
+ msg.erase(msg.size() - 1);
+
+ if (m_request != NULL)
+ CLog::Log(LOGERROR, "WSGI [{}]: {}", m_request->url, msg);
+ else
+ CLog::Log(LOGERROR, "WSGI: {}", msg);
+ }
+
+ void WsgiErrorStream::writelines(const std::vector<String>& seq)
+ {
+ if (seq.empty())
+ return;
+
+ String msg = StringUtils::Join(seq, "");
+ write(msg);
+ }
+
+#ifndef SWIG
+ void WsgiErrorStream::SetRequest(HTTPPythonRequest* request)
+ {
+ if (m_request != NULL)
+ return;
+
+ m_request = request;
+ }
+#endif
+ }
+}
diff --git a/xbmc/interfaces/legacy/wsgi/WsgiErrorStream.h b/xbmc/interfaces/legacy/wsgi/WsgiErrorStream.h
new file mode 100644
index 0000000..e9e7694
--- /dev/null
+++ b/xbmc/interfaces/legacy/wsgi/WsgiErrorStream.h
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2015-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "interfaces/legacy/AddonClass.h"
+
+#include <vector>
+
+struct HTTPPythonRequest;
+
+namespace XBMCAddon
+{
+ namespace xbmcwsgi
+ {
+
+ /// \defgroup python_xbmcwsgi_WsgiErrorStream WsgiErrorStream
+ /// \ingroup python_xbmcwsgi
+ /// @{
+ /// @brief **Represents the wsgi.errors stream to write error messages.**
+ ///
+ /// \python_class{ WsgiErrorStream() }
+ ///
+ /// This implementation writes the error messages to the application's log
+ /// file.
+ ///
+ ///-------------------------------------------------------------------------
+ ///
+ class WsgiErrorStream : public AddonClass
+ {
+ public:
+ WsgiErrorStream();
+ ~WsgiErrorStream() override;
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcwsgi_WsgiErrorStream
+ /// \python_func{ flush() }
+ /// Since nothing is buffered this is a no-op.
+ ///
+ ///
+ flush();
+#else
+ inline void flush() { }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcwsgi_WsgiErrorStream
+ /// \python_func{ write(str) }
+ /// Writes the given error message to the application's log file.
+ ///
+ /// @param str A string to save in log file
+ ///
+ /// @note A trailing `\n` is removed.
+ ///
+ write(...);
+#else
+ void write(const String& str);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcwsgi_WsgiErrorStream
+ /// \python_func{ writelines(seq) }
+ /// Joins the given list of error messages (without any separator) into
+ /// a single error message which is written to the application's log file.
+ ///
+ /// @param seq A list of strings which will be logged.
+ ///
+ writelines(...);
+#else
+ void writelines(const std::vector<String>& seq);
+#endif
+
+#ifndef SWIG
+ /**
+ * Sets the given request.
+ */
+ void SetRequest(HTTPPythonRequest* request);
+
+ HTTPPythonRequest* m_request;
+#endif
+ };
+ /// @}
+ }
+}
diff --git a/xbmc/interfaces/legacy/wsgi/WsgiInputStream.cpp b/xbmc/interfaces/legacy/wsgi/WsgiInputStream.cpp
new file mode 100644
index 0000000..5146007
--- /dev/null
+++ b/xbmc/interfaces/legacy/wsgi/WsgiInputStream.cpp
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2015-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "WsgiInputStream.h"
+
+#include "network/httprequesthandler/python/HTTPPythonRequest.h"
+#include "utils/StringUtils.h"
+
+namespace XBMCAddon
+{
+ namespace xbmcwsgi
+ {
+ WsgiInputStreamIterator::WsgiInputStreamIterator()
+ : m_data(),
+ m_line()
+ { }
+
+#ifndef SWIG
+ WsgiInputStreamIterator::WsgiInputStreamIterator(const String& data, bool end /* = false */)
+ : m_data(data),
+ m_remaining(end ? 0 : data.size()),
+ m_line()
+ { }
+#endif
+
+ WsgiInputStreamIterator::~WsgiInputStreamIterator() = default;
+
+ String WsgiInputStreamIterator::read(unsigned long size /* = 0 */) const
+ {
+ // make sure we don't try to read more data than we have
+ if (size <= 0 || size > m_remaining)
+ size = m_remaining;
+
+ // remember the current read offset
+ size_t offset = static_cast<size_t>(m_offset);
+
+ // adjust the read offset and the remaining data length
+ m_offset += size;
+ m_remaining -= size;
+
+ // return the data being requested
+ return m_data.substr(offset, size);
+ }
+
+ String WsgiInputStreamIterator::readline(unsigned long size /* = 0 */) const
+ {
+ // make sure we don't try to read more data than we have
+ if (size <= 0 || size > m_remaining)
+ size = m_remaining;
+
+ size_t offset = static_cast<size_t>(m_offset);
+ size_t pos = m_data.find('\n', offset);
+
+ // make sure pos has a valid value and includes the \n character
+ if (pos == std::string::npos)
+ pos = m_data.size();
+ else
+ pos += 1;
+
+ if (pos - offset < size)
+ size = pos - offset;
+
+ // read the next line
+ String line = read(size);
+
+ // remove any trailing \r\n
+ StringUtils::TrimRight(line, "\r\n");
+
+ return line;
+ }
+
+ std::vector<String> WsgiInputStreamIterator::readlines(unsigned long sizehint /* = 0 */) const
+ {
+ std::vector<String> lines;
+
+ // make sure we don't try to read more data than we have
+ if (sizehint <= 0 || sizehint > m_remaining)
+ sizehint = m_remaining;
+
+ do
+ {
+ // read a full line
+ String line = readline();
+
+ // adjust the sizehint by the number of bytes just read
+ sizehint -= line.length();
+
+ // add it to the list of read lines
+ lines.push_back(line);
+ } while (sizehint > 0);
+
+ return lines;
+ }
+
+#ifndef SWIG
+ WsgiInputStreamIterator& WsgiInputStreamIterator::operator++()
+ {
+ m_line.clear();
+
+ if (!end())
+ {
+ // read the next line
+ m_line = readline();
+ }
+
+ return *this;
+ }
+
+ bool WsgiInputStreamIterator::operator==(const WsgiInputStreamIterator& rhs)
+ {
+ return m_data == rhs.m_data &&
+ m_offset == rhs.m_offset &&
+ m_remaining == rhs.m_remaining;
+ }
+
+ bool WsgiInputStreamIterator::operator!=(const WsgiInputStreamIterator& rhs)
+ {
+ return !(*this == rhs);
+ }
+
+ String& WsgiInputStreamIterator::operator*()
+ {
+ return m_line;
+ }
+#endif
+
+ WsgiInputStream::WsgiInputStream()
+ : m_request(NULL)
+ { }
+
+ WsgiInputStream::~WsgiInputStream()
+ {
+ m_request = NULL;
+ }
+
+#ifndef SWIG
+ WsgiInputStreamIterator* WsgiInputStream::begin()
+ {
+ return new WsgiInputStreamIterator(m_data, false);
+ }
+
+ WsgiInputStreamIterator* WsgiInputStream::end()
+ {
+ return new WsgiInputStreamIterator(m_data, true);
+ }
+
+ void WsgiInputStream::SetRequest(HTTPPythonRequest* request)
+ {
+ if (m_request != NULL)
+ return;
+
+ m_request = request;
+
+ // set the remaining bytes to be read
+ m_data = m_request->requestContent;
+ m_offset = 0;
+ m_remaining = m_data.size();
+ }
+#endif
+ }
+}
diff --git a/xbmc/interfaces/legacy/wsgi/WsgiInputStream.h b/xbmc/interfaces/legacy/wsgi/WsgiInputStream.h
new file mode 100644
index 0000000..d7bf73f
--- /dev/null
+++ b/xbmc/interfaces/legacy/wsgi/WsgiInputStream.h
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2015-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "interfaces/legacy/AddonClass.h"
+
+#include <vector>
+
+struct HTTPPythonRequest;
+
+namespace XBMCAddon
+{
+ namespace xbmcwsgi
+ {
+
+ // Iterator for the wsgi.input stream.
+ class WsgiInputStreamIterator : public AddonClass
+ {
+ public:
+ WsgiInputStreamIterator();
+ ~WsgiInputStreamIterator() override;
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcwsgi_WsgiInputStream
+ /// \python_func{ read([size]) }
+ ///
+ /// Read a maximum of `<size>` bytes from the wsgi.input stream.
+ ///
+ /// @param size [opt] bytes to read
+ /// @return Returns the readed string
+ ///
+ read(...);
+#else
+ String read(unsigned long size = 0) const;
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcwsgi_WsgiInputStream
+ /// \python_func{ readline([size]) }
+ ///
+ /// Read a full line up to a maximum of `<size>` bytes from the wsgi.input
+ /// stream.
+ ///
+ /// @param size [opt] bytes to read
+ /// @return Returns the readed string line
+ ///
+ read(...);
+#else
+ String readline(unsigned long size = 0) const;
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcwsgi_WsgiInputStream
+ /// \python_func{ readlines([sizehint]) }
+ ///
+ /// Read multiple full lines up to at least `<sizehint>` bytes from the
+ /// wsgi.input stream and return them as a list.
+ ///
+ /// @param sizehint [opt] bytes to read
+ /// @return Returns a list readed string lines
+ ///
+ read(...);
+#else
+ std::vector<String> readlines(unsigned long sizehint = 0) const;
+#endif
+
+#if !defined SWIG && !defined DOXYGEN_SHOULD_SKIP_THIS
+ WsgiInputStreamIterator(const String& data, bool end = false);
+
+ WsgiInputStreamIterator& operator++();
+ bool operator==(const WsgiInputStreamIterator& rhs);
+ bool operator!=(const WsgiInputStreamIterator& rhs);
+ String& operator*();
+ inline bool end() const { return m_remaining <= 0; }
+
+ protected:
+ String m_data;
+ mutable unsigned long m_offset = 0;
+ mutable unsigned long m_remaining = 0;
+
+ private:
+ String m_line;
+#endif
+ };
+
+ /// \defgroup python_xbmcwsgi_WsgiInputStream WsgiInputStream
+ /// \ingroup python_xbmcwsgi
+ /// @{
+ /// @brief **Represents the wsgi.input stream to access data from a HTTP request.**
+ ///
+ /// \python_class{ WsgiInputStream() }
+ ///
+ ///-------------------------------------------------------------------------
+ ///
+ class WsgiInputStream : public WsgiInputStreamIterator
+ {
+ public:
+ WsgiInputStream();
+ ~WsgiInputStream() override;
+
+#if !defined SWIG && !defined DOXYGEN_SHOULD_SKIP_THIS
+ WsgiInputStreamIterator* begin();
+ WsgiInputStreamIterator* end();
+
+ /**
+ * Sets the given request.
+ */
+ void SetRequest(HTTPPythonRequest* request);
+
+ HTTPPythonRequest* m_request;
+#endif
+ };
+ }
+}
diff --git a/xbmc/interfaces/legacy/wsgi/WsgiResponse.cpp b/xbmc/interfaces/legacy/wsgi/WsgiResponse.cpp
new file mode 100644
index 0000000..175e83d
--- /dev/null
+++ b/xbmc/interfaces/legacy/wsgi/WsgiResponse.cpp
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2015-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "WsgiResponse.h"
+
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <inttypes.h>
+#include <utility>
+
+namespace XBMCAddon
+{
+ namespace xbmcwsgi
+ {
+ WsgiResponse::WsgiResponse()
+ : m_responseHeaders(),
+ m_body()
+ { }
+
+ WsgiResponse::~WsgiResponse() = default;
+
+ WsgiResponseBody* WsgiResponse::operator()(const String& status, const std::vector<WsgiHttpHeader>& response_headers, void* exc_info /* = NULL */)
+ {
+ if (m_called)
+ {
+ CLog::Log(LOGWARNING, "WsgiResponse: callable has already been called");
+ return NULL;
+ }
+
+ m_called = true;
+
+ // parse the status
+ if (!status.empty())
+ {
+ std::vector<String> statusParts = StringUtils::Split(status, ' ', 2);
+ if (statusParts.size() == 2 && StringUtils::IsNaturalNumber(statusParts.front()))
+ {
+ int64_t parsedStatus = strtol(statusParts.front().c_str(), NULL, 0);
+ if (parsedStatus >= MHD_HTTP_OK && parsedStatus <= MHD_HTTP_NOT_EXTENDED)
+ m_status = static_cast<int>(parsedStatus);
+ else
+ CLog::Log(LOGWARNING, "WsgiResponse: invalid status number {} in \"{}\" provided",
+ parsedStatus, status);
+ }
+ else
+ CLog::Log(LOGWARNING, "WsgiResponse: invalid status \"{}\" provided", status);
+ }
+ else
+ CLog::Log(LOGWARNING, "WsgiResponse: empty status provided");
+
+ // copy the response headers
+ for (const auto& headerIt : response_headers)
+ m_responseHeaders.insert({headerIt.first(), headerIt.second()});
+
+ return &m_body;
+ }
+
+#ifndef SWIG
+ void WsgiResponse::Append(const std::string& data)
+ {
+ if (!data.empty())
+ m_body.m_data.append(data);
+ }
+
+ bool WsgiResponse::Finalize(HTTPPythonRequest* request) const
+ {
+ if (request == NULL || !m_called)
+ return false;
+
+ // copy the response status
+ request->responseStatus = m_status;
+
+ // copy the response headers
+ if (m_status >= MHD_HTTP_OK && m_status < MHD_HTTP_BAD_REQUEST)
+ request->responseHeaders.insert(m_responseHeaders.begin(), m_responseHeaders.end());
+ else
+ request->responseHeadersError.insert(m_responseHeaders.begin(), m_responseHeaders.end());
+
+ // copy the body
+ request->responseData = m_body.m_data;
+
+ return true;
+ }
+#endif
+ }
+}
diff --git a/xbmc/interfaces/legacy/wsgi/WsgiResponse.h b/xbmc/interfaces/legacy/wsgi/WsgiResponse.h
new file mode 100644
index 0000000..412e520
--- /dev/null
+++ b/xbmc/interfaces/legacy/wsgi/WsgiResponse.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2015-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "interfaces/legacy/AddonClass.h"
+#include "interfaces/legacy/Tuple.h"
+#include "interfaces/legacy/wsgi/WsgiResponseBody.h"
+#include "network/httprequesthandler/python/HTTPPythonRequest.h"
+
+#include <vector>
+
+namespace XBMCAddon
+{
+ namespace xbmcwsgi
+ {
+ typedef Tuple<String, String> WsgiHttpHeader;
+
+ /// \defgroup python_xbmcwsgi_WsgiResponse WsgiResponse
+ /// \ingroup python_xbmcwsgi
+ /// @{
+ /// @brief **Represents the start_response callable passed to a WSGI handler.**
+ ///
+ /// \python_class{ WsgiResponse() }
+ ///
+ ///-------------------------------------------------------------------------
+ ///
+ class WsgiResponse : public AddonClass
+ {
+ public:
+ WsgiResponse();
+ ~WsgiResponse() override;
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcwsgi_WsgiInputStreamIterator
+ /// \python_func{ operator(status, response_headers[, exc_info]) }
+ ///
+ /// Callable implementation to initialize the response with the given
+ /// HTTP status and the HTTP response headers.
+ ///
+ /// @param status an HTTP status string like 200 OK or 404
+ /// Not Found.
+ /// @param response_headers a list of (header_name, header_value)
+ /// tuples. It must be a Python list. Each
+ /// header_name must be a valid HTTP header
+ /// field-name (as
+ /// @param exc_info [optional] python sys.exc_info() tuple.
+ /// This argument should be supplied by the
+ /// application only if start_response is
+ /// being called by an error
+ /// @return The write() method \ref python_xbmcwsgi_WsgiResponseBody "WsgiResponseBody"
+ ///
+ operator(...);
+#else
+ WsgiResponseBody* operator()(const String& status, const std::vector<WsgiHttpHeader>& response_headers, void* exc_info = NULL);
+#endif
+
+#ifndef SWIG
+ void Append(const std::string& data);
+
+ bool Finalize(HTTPPythonRequest* request) const;
+
+ private:
+ bool m_called = false;
+ int m_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ std::multimap<std::string, std::string> m_responseHeaders;
+
+ WsgiResponseBody m_body;
+#endif
+ };
+ }
+}
diff --git a/xbmc/interfaces/legacy/wsgi/WsgiResponseBody.cpp b/xbmc/interfaces/legacy/wsgi/WsgiResponseBody.cpp
new file mode 100644
index 0000000..2e84319
--- /dev/null
+++ b/xbmc/interfaces/legacy/wsgi/WsgiResponseBody.cpp
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2015-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "WsgiResponseBody.h"
+
+namespace XBMCAddon
+{
+ namespace xbmcwsgi
+ {
+ WsgiResponseBody::WsgiResponseBody()
+ : m_data()
+ { }
+
+ WsgiResponseBody::~WsgiResponseBody() = default;
+
+ void WsgiResponseBody::operator()(const String& data)
+ {
+ if (data.empty())
+ return;
+
+ m_data.append(data);
+ }
+ }
+}
diff --git a/xbmc/interfaces/legacy/wsgi/WsgiResponseBody.h b/xbmc/interfaces/legacy/wsgi/WsgiResponseBody.h
new file mode 100644
index 0000000..4f18583
--- /dev/null
+++ b/xbmc/interfaces/legacy/wsgi/WsgiResponseBody.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2015-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "interfaces/legacy/AddonClass.h"
+
+namespace XBMCAddon
+{
+ namespace xbmcwsgi
+ {
+ /// \defgroup python_xbmcwsgi_WsgiResponseBody WsgiResponseBody
+ /// \ingroup python_xbmcwsgi
+ /// @{
+ /// @brief **Represents the write callable returned by the start_response callable passed to a WSGI handler.**
+ ///
+ /// \python_class{ WsgiResponseBody() }
+ ///
+ ///-------------------------------------------------------------------------
+ ///
+ class WsgiResponseBody : public AddonClass
+ {
+ public:
+ WsgiResponseBody();
+ ~WsgiResponseBody() override;
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcwsgi_WsgiInputStreamIterator
+ /// \python_func{ operator(status, response_headers[, exc_info]) }
+ ///
+ /// Callable implementation to write data to the response.
+ ///
+ /// @param data string data to write
+ ///
+ operator()(...);
+#else
+ void operator()(const String& data);
+#endif
+
+#if !defined SWIG && !defined DOXYGEN_SHOULD_SKIP_THIS
+ String m_data;
+#endif
+ };
+ }
+}