web::mime::ContentMappers: implement selectMapper() method

This commit is contained in:
Leonid Stryzhevskyi 2024-05-13 04:45:09 +03:00
parent 9ec854ebbb
commit c8dcb2238e
4 changed files with 293 additions and 35 deletions

View File

@ -34,6 +34,10 @@ v_int32 Conversion::strToInt32(const char* str){
}
v_int32 Conversion::strToInt32(const oatpp::String& str, bool& success){
if(str == nullptr || str->empty()) {
success = false;
return 0;
}
char* end;
v_int32 result = static_cast<v_int32>(std::strtol(str->data(), &end, 10));
success = ((reinterpret_cast<v_buff_size>(end) - reinterpret_cast<v_buff_size>(str->data())) == static_cast<v_buff_size>(str->size()));
@ -46,6 +50,10 @@ v_uint32 Conversion::strToUInt32(const char* str){
}
v_uint32 Conversion::strToUInt32(const oatpp::String& str, bool& success){
if(str == nullptr || str->empty()) {
success = false;
return 0;
}
char* end;
v_uint32 result = static_cast<v_uint32>(std::strtoul(str->data(), &end, 10));
success = ((reinterpret_cast<v_buff_size>(end) - reinterpret_cast<v_buff_size>(str->data())) == static_cast<v_buff_size>(str->size()));
@ -58,6 +66,10 @@ v_int64 Conversion::strToInt64(const char* str){
}
v_int64 Conversion::strToInt64(const oatpp::String& str, bool& success){
if(str == nullptr || str->empty()) {
success = false;
return 0;
}
char* end;
v_int64 result = std::strtoll(str->data(), &end, 10);
success = ((reinterpret_cast<v_buff_size>(end) - reinterpret_cast<v_buff_size>(str->data())) == static_cast<v_buff_size>(str->size()));
@ -70,6 +82,10 @@ v_uint64 Conversion::strToUInt64(const char* str){
}
v_uint64 Conversion::strToUInt64(const oatpp::String& str, bool& success){
if(str == nullptr || str->empty()) {
success = false;
return 0;
}
char* end;
v_uint64 result = std::strtoull(str->data(), &end, 10);
success = ((reinterpret_cast<v_buff_size>(end) - reinterpret_cast<v_buff_size>(str->data())) == static_cast<v_buff_size>(str->size()));
@ -170,6 +186,10 @@ v_float32 Conversion::strToFloat32(const char* str){
}
v_float32 Conversion::strToFloat32(const oatpp::String& str, bool& success) {
if(str == nullptr || str->empty()) {
success = false;
return 0;
}
char* end;
v_float32 result = std::strtof(str->data(), &end);
success = ((reinterpret_cast<v_buff_size>(end) - reinterpret_cast<v_buff_size>(str->data())) == static_cast<v_buff_size>(str->size()));
@ -182,6 +202,10 @@ v_float64 Conversion::strToFloat64(const char* str){
}
v_float64 Conversion::strToFloat64(const oatpp::String& str, bool& success) {
if(str == nullptr || str->empty()) {
success = false;
return 0;
}
char* end;
v_float64 result = std::strtod(str->data(), &end);
success = ((reinterpret_cast<v_buff_size>(end) - reinterpret_cast<v_buff_size>(str->data())) == static_cast<v_buff_size>(str->size()));

View File

@ -24,28 +24,54 @@
#include "ContentMappers.hpp"
namespace oatpp { namespace web { namespace mime {
#include "oatpp/utils/parser/Caret.hpp"
#include "oatpp/utils/Conversion.hpp"
#include <algorithm>
namespace oatpp::web::mime {
std::pair<oatpp::String, oatpp::String> ContentMappers::typeAndSubtype(const data::share::StringKeyLabelCI& contentType) const {
if(contentType == nullptr || contentType.getSize() == 0) return {};
utils::parser::Caret caret(reinterpret_cast<const char*>(contentType.getData()), contentType.getSize());
auto typeL = caret.putLabel();
if(!caret.findChar('/')) return {};
typeL.end();
caret.canContinueAtChar('/', 1);
auto stypeL = caret.putLabel();
caret.findCharFromSet(";, \r\n\t");
return {typeL.toString(), stypeL.toString()};
}
void ContentMappers::putMapper(const std::shared_ptr<data::mapping::ObjectMapper>& mapper) {
std::unique_lock lock(m_mutex);
if(m_defaultMapper == nullptr) {
m_defaultMapper = mapper;
}
m_index[mapper->getInfo().mimeType][mapper->getInfo().mimeSubtype] = mapper;
m_mappers[mapper->getInfo().httpContentType] = mapper;
}
void ContentMappers::setDefaultMapper(const oatpp::String& contentType) {
std::unique_lock lock(m_mutex);
if(m_defaultMapper == nullptr) {
m_defaultMapper = m_mappers.at(contentType);
}
m_defaultMapper = m_mappers.at(contentType);
}
void ContentMappers::setDefaultMapper(const std::shared_ptr<data::mapping::ObjectMapper>& mapper) {
std::unique_lock lock(m_mutex);
m_defaultMapper = mapper;
if(m_defaultMapper) {
m_mappers[m_defaultMapper->getInfo().httpContentType] = m_defaultMapper;
m_index[m_defaultMapper->getInfo().mimeType][m_defaultMapper->getInfo().mimeSubtype] = m_defaultMapper;
m_mappers[mapper->getInfo().httpContentType] = m_defaultMapper;
}
}
@ -64,20 +90,86 @@ std::shared_ptr<data::mapping::ObjectMapper> ContentMappers::getDefaultMapper()
}
std::shared_ptr<data::mapping::ObjectMapper> ContentMappers::selectMapper(const protocol::http::HeaderValueData& values) const {
// TODO select
return m_defaultMapper;
for(auto& t : values.tokens) {
if(t == "*/*") return m_defaultMapper;
auto tst = typeAndSubtype(t);
if(!tst.first || !tst.second) continue;
auto it = m_index.find(tst.first);
if(it == m_index.end()) continue;
auto ist = it->second.find(tst.second);
if(ist != it->second.end()) {
return ist->second; // return immediately - quality = 1
} else if(tst.second == "*" && !it->second.empty()) {
if(m_defaultMapper && tst.first == m_defaultMapper->getInfo().mimeType) {
return m_defaultMapper;
}
return it->second.begin()->second; // return immediately - quality = 1
}
}
std::vector<MatchedMapper> matches;
for(auto& tp : values.titleParams) {
auto tst = typeAndSubtype(tp.first);
if(!tst.first || !tst.second) continue;
bool success;
auto q = utils::Conversion::strToFloat64(tp.second.toString(), success);
if(!success) continue;
if(tst.first == "*" && tst.second == "*") {
matches.push_back({m_defaultMapper, q});
continue;
}
auto it = m_index.find(tst.first);
if(it == m_index.end()) continue;
auto ist = it->second.find(tst.second);
if(ist != it->second.end()) {
matches.push_back({ist->second, q});
continue;
} else if(tst.second == "*" && !it->second.empty()) {
if(m_defaultMapper && tst.first == m_defaultMapper->getInfo().mimeType) {
matches.push_back({m_defaultMapper, q});
} else {
matches.push_back({it->second.begin()->second, q});
}
continue;
}
}
if(matches.empty()) {
return nullptr;
}
std::sort(matches.begin(), matches.end());
return matches.at(0).mapper;
}
std::shared_ptr<data::mapping::ObjectMapper> ContentMappers::selectMapper(const oatpp::String& contentType) const {
std::shared_ptr<data::mapping::ObjectMapper> ContentMappers::selectMapper(const oatpp::String& acceptHeader) const {
std::shared_lock lock(m_mutex);
if(!contentType || contentType->empty()) {
if(!acceptHeader || acceptHeader->empty()) {
return m_defaultMapper;
}
protocol::http::HeaderValueData values;
protocol::http::Parser::parseHeaderValueData(values, contentType, ',');
protocol::http::Parser::parseHeaderValueData(values, acceptHeader, ',');
return selectMapper(values);
@ -93,6 +185,7 @@ std::shared_ptr<data::mapping::ObjectMapper> ContentMappers::selectMapper(const
protocol::http::HeaderValueData values;
for(auto& ct : acceptableContentTypes) {
if(ct == nullptr || ct->empty()) continue;
protocol::http::Parser::parseHeaderValueData(values, ct, ',');
}
@ -106,4 +199,4 @@ void ContentMappers::clear() {
m_mappers.clear();
}
}}}
}

View File

@ -34,9 +34,26 @@ namespace oatpp::web::mime {
class ContentMappers {
private:
struct MatchedMapper {
std::shared_ptr<data::mapping::ObjectMapper> mapper;
v_float64 quality;
bool operator < (const MatchedMapper& other) const {
return quality > other.quality;
}
};
private:
typedef std::unordered_map<data::share::StringKeyLabelCI, std::shared_ptr<data::mapping::ObjectMapper>> MappersBySubtypes;
private:
std::pair<oatpp::String, oatpp::String> typeAndSubtype(const data::share::StringKeyLabelCI& contentType) const;
std::shared_ptr<data::mapping::ObjectMapper> selectMapper(const protocol::http::HeaderValueData& values) const;
private:
std::unordered_map<oatpp::String, std::shared_ptr<data::mapping::ObjectMapper>> m_mappers;
std::unordered_map<data::share::StringKeyLabelCI, MappersBySubtypes> m_index;
std::unordered_map<data::share::StringKeyLabelCI, std::shared_ptr<data::mapping::ObjectMapper>> m_mappers;
std::shared_ptr<data::mapping::ObjectMapper> m_defaultMapper;
mutable std::shared_mutex m_mutex;
public:
@ -52,7 +69,7 @@ public:
std::shared_ptr<data::mapping::ObjectMapper> getMapper(const oatpp::String& contentType) const;
std::shared_ptr<data::mapping::ObjectMapper> getDefaultMapper() const;
std::shared_ptr<data::mapping::ObjectMapper> selectMapper(const oatpp::String& contentType) const;
std::shared_ptr<data::mapping::ObjectMapper> selectMapper(const oatpp::String& acceptHeader) const;
std::shared_ptr<data::mapping::ObjectMapper> selectMapper(const std::vector<oatpp::String>& acceptableContentTypes) const;
void clear();

View File

@ -31,37 +31,161 @@
namespace oatpp::web::mime {
namespace {
class FakeMapper : public data::mapping::ObjectMapper {
public:
FakeMapper(const oatpp::String& mimeType, const oatpp::String& mimeSubtype)
: ObjectMapper(Info(mimeType, mimeSubtype))
{}
void write(data::stream::ConsistentOutputStream* stream, const oatpp::Void& variant, data::mapping::ErrorStack& errorStack) const override {
// DO NOTHING
}
oatpp::Void read(oatpp::utils::parser::Caret& caret, const oatpp::Type* type, data::mapping::ErrorStack& errorStack) const override {
return nullptr;
}
};
}
void ContentMappersTest::onRun() {
protocol::http::incoming::Request request(nullptr, {}, {}, nullptr, nullptr);
request.putHeader("Content-Type", "application/json");
request.putHeader("Content-Length", "1000");
request.putHeader("Accept", "application/*");
request.putHeader("Accept", "application/json");
request.putHeader("Accept", "text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8");
auto values = request.getHeaderValues("Accept");
protocol::http::HeaderValueData data;
for(auto& v : values) {
protocol::http::Parser::parseHeaderValueData(data, v, ',');
OATPP_LOGD(TAG, "value='%s'", v->c_str())
{
OATPP_LOGD(TAG, "case 1 - default mapper")
ContentMappers mappers;
mappers.putMapper(std::make_shared<FakeMapper>("application", "json"));
auto m = mappers.getMapper("APPLICATION/JSON");
OATPP_ASSERT(m != nullptr)
OATPP_ASSERT(m->getInfo().httpContentType == "application/json")
OATPP_ASSERT(mappers.getDefaultMapper().get() == m.get())
}
OATPP_LOGD(TAG, "")
{
OATPP_LOGD(TAG, "case 2 - default mapper")
ContentMappers mappers;
mappers.putMapper(std::make_shared<FakeMapper>("application", "json"));
mappers.putMapper(std::make_shared<FakeMapper>("text", "html"));
for(auto& t : data.tokens) {
OATPP_LOGD(TAG, "token='%s'", t.toString()->c_str())
auto m = mappers.getMapper("text/HTML");
auto d = mappers.getDefaultMapper();
OATPP_ASSERT(m != nullptr)
OATPP_ASSERT(m->getInfo().httpContentType == "text/html")
OATPP_ASSERT(d->getInfo().httpContentType == "application/json")
}
for(auto& p : data.titleParams) {
OATPP_LOGD(TAG, "'%s'='%s'", p.first.toString()->c_str(), p.second.toString()->c_str())
{
OATPP_LOGD(TAG, "case 3 - default mapper")
ContentMappers mappers;
mappers.putMapper(std::make_shared<FakeMapper>("application", "json"));
mappers.putMapper(std::make_shared<FakeMapper>("text", "html"));
mappers.setDefaultMapper("text/html");
auto m = mappers.getMapper("application/JSON");
auto d = mappers.getDefaultMapper();
OATPP_ASSERT(m != nullptr)
OATPP_ASSERT(m->getInfo().httpContentType == "application/json")
OATPP_ASSERT(d->getInfo().httpContentType == "text/html")
}
ContentMappers mappers;
{
OATPP_LOGD(TAG, "case 4 - select mapper")
ContentMappers mappers;
mappers.putMapper(std::make_shared<FakeMapper>("application", "json"));
mappers.putMapper(std::make_shared<FakeMapper>("text", "html"));
auto m= mappers.selectMapper(std::vector<oatpp::String>{
"application/json",
"text/html",
}
);
OATPP_ASSERT(m->getInfo().httpContentType == "application/json" || m->getInfo().httpContentType == "text/html")
}
{
OATPP_LOGD(TAG, "case 5 - select mapper")
ContentMappers mappers;
mappers.putMapper(std::make_shared<FakeMapper>("application", "json"));
mappers.putMapper(std::make_shared<FakeMapper>("text", "html"));
auto m= mappers.selectMapper(std::vector<oatpp::String>{
"application/json;q=0.9",
"text/html;q=0.1",
}
);
OATPP_ASSERT(m->getInfo().httpContentType == "application/json")
}
{
OATPP_LOGD(TAG, "case 6 - select mapper")
ContentMappers mappers;
mappers.putMapper(std::make_shared<FakeMapper>("application", "json"));
mappers.putMapper(std::make_shared<FakeMapper>("text", "html"));
auto m= mappers.selectMapper(std::vector<oatpp::String>{
"application/json;q=0.1",
"text/html;q=0.9",
}
);
OATPP_ASSERT(m->getInfo().httpContentType == "text/html")
}
{
OATPP_LOGD(TAG, "case 7 - select mapper - corrupted input")
ContentMappers mappers;
mappers.putMapper(std::make_shared<FakeMapper>("application", "json"));
mappers.putMapper(std::make_shared<FakeMapper>("text", "html"));
auto m= mappers.selectMapper(std::vector<oatpp::String>{
"application/json;<anything>=0.5",
"text/html;q=",
}
);
OATPP_ASSERT(m->getInfo().httpContentType == "application/json")
}
ContentMappers richMappers;
richMappers.putMapper(std::make_shared<FakeMapper>("application", "json"));
richMappers.putMapper(std::make_shared<FakeMapper>("application", "xml"));
richMappers.putMapper(std::make_shared<FakeMapper>("application", "octet-stream"));
richMappers.putMapper(std::make_shared<FakeMapper>("text", "css"));
richMappers.putMapper(std::make_shared<FakeMapper>("text", "csv"));
richMappers.putMapper(std::make_shared<FakeMapper>("text", "html"));
{
OATPP_LOGD(TAG, "case 8 - select mapper - empty")
auto m = richMappers.selectMapper(
""
);
OATPP_ASSERT(m->getInfo().httpContentType == "application/json")
}
{
OATPP_LOGD(TAG, "case 9 - select mapper - corrupted input")
auto m = richMappers.selectMapper(
"application/*;q=0.8, text/*;q=0.9, *.*"
);
OATPP_ASSERT(m->getInfo().mimeType == "text")
}
{
OATPP_LOGD(TAG, "case 10 - select mapper ")
auto m = richMappers.selectMapper(
"application/*;q=0.8, text/*;q=0.9, */*"
);
OATPP_ASSERT(m->getInfo().httpContentType == "application/json")
}
{
OATPP_LOGD(TAG, "case 11 - select mapper ")
auto m = richMappers.selectMapper(
"application/*;q=0.9, text/*;q=0.8"
);
OATPP_ASSERT(m->getInfo().httpContentType == "application/json")
}
}