diff options
author | Ilya Zverev <zverik@textual.ru> | 2016-11-24 20:27:51 +0300 |
---|---|---|
committer | Ilya Zverev <zverik@textual.ru> | 2016-11-30 18:55:40 +0300 |
commit | 153e4f2b8c0ea07b90b3f72455f201754577e751 (patch) | |
tree | 9c5809357572f6964dde7a9aa39c66eb8b12e3c4 /indexer | |
parent | 70871724edcb0a50dde550512310fda1a370566f (diff) |
[banners] Add banners infrastructure
Diffstat (limited to 'indexer')
-rw-r--r-- | indexer/CMakeLists.txt | 2 | ||||
-rw-r--r-- | indexer/banners.cpp | 224 | ||||
-rw-r--r-- | indexer/banners.hpp | 59 | ||||
-rw-r--r-- | indexer/feature_meta.cpp | 3 | ||||
-rw-r--r-- | indexer/feature_meta.hpp | 1 | ||||
-rw-r--r-- | indexer/indexer.pro | 2 | ||||
-rw-r--r-- | indexer/indexer_tests/banners_test.cpp | 70 | ||||
-rw-r--r-- | indexer/indexer_tests/indexer_tests.pro | 1 | ||||
-rw-r--r-- | indexer/map_object.hpp | 1 |
9 files changed, 363 insertions, 0 deletions
diff --git a/indexer/CMakeLists.txt b/indexer/CMakeLists.txt index e239721de0..3ae176faf9 100644 --- a/indexer/CMakeLists.txt +++ b/indexer/CMakeLists.txt @@ -6,6 +6,8 @@ set( SRC altitude_loader.cpp altitude_loader.hpp + banners.cpp + banners.hpp categories_holder_loader.cpp categories_holder.cpp categories_holder.hpp diff --git a/indexer/banners.cpp b/indexer/banners.cpp new file mode 100644 index 0000000000..509d069f4b --- /dev/null +++ b/indexer/banners.cpp @@ -0,0 +1,224 @@ +#include "indexer/banners.hpp" +#include "indexer/classificator.hpp" +#include "indexer/feature.hpp" + +#include "platform/platform.hpp" + +#include "coding/reader_streambuf.hpp" + +#include "base/stl_add.hpp" +#include "base/string_utils.hpp" + +#include "defines.hpp" + +namespace +{ +time_t constexpr kEternity = 0; + +// Convert ISO date to unix time. +bool StringToTimestamp(string const & s, time_t & result) +{ + istringstream is(s); + tm time; + is >> get_time(&time, "%F"); + CHECK(!is.fail(), ("Wrong date format:", s, "(expecting YYYY-MM-DD)")); + + time.tm_sec = time.tm_min = time.tm_hour = 0; + + time_t timestamp = mktime(&time); + if (timestamp < 0) + return false; + + result = timestamp; + return true; +} +} // namespace + +namespace banner +{ +string Banner::GetProperty(string const & name) const +{ + if (name == "lang") + { + return "ru"; // TODO(@zverik): this variable, {mwmlang}, {country} etc. + } + else + { + auto const property = m_properties.find(name); + if (property != m_properties.end()) + return property->second; + } + return {}; +} + +Banner::Banner(string const & id) : m_id(id) +{ + m_messageBase = "banner_" + id; + m_iconName = m_messageBase + ".png"; + m_defaultUrl = ""; + m_activeAfter = time(nullptr); + m_activeBefore = kEternity; +} + +bool Banner::IsActive() const +{ + if (IsEmpty()) + return false; + time_t const now = time(nullptr); + return now >= m_activeAfter && (m_activeBefore == kEternity || now < m_activeBefore); +} + +void Banner::SetProperty(string const & name, string const & value) +{ + if (name == "messages") + { + m_messageBase = value; + } + else if (name == "icon") + { + m_iconName = value; + } + else if (name == "url") + { + CHECK(strings::StartsWith(value, "http://") || strings::StartsWith(value, "https://"), + ("URL without a protocol for banner", m_id)); + m_defaultUrl = value; + } + else if (name == "start") + { + CHECK(StringToTimestamp(value, m_activeAfter), ("Wrong start date", value, "for banner", m_id)); + } + else if (name == "end") + { + CHECK(StringToTimestamp(value, m_activeBefore), ("Wrong end date", value, "for banner", m_id)); + m_activeBefore += 24 * 60 * 60; // Add a day so we don't miss one + } + else + { + m_properties.emplace(make_pair(name, value)); + } +} + +string Banner::GetFormattedUrl(string const & url) const +{ + string baseUrl = url.empty() ? m_defaultUrl : url; + auto start = baseUrl.find('{'); + while (start != string::npos) + { + auto end = baseUrl.find('}', start + 1); + if (end == string::npos) + break; + string value = GetProperty(baseUrl.substr(start + 1, end - start - 1)); + if (!value.empty()) + { + baseUrl.replace(start, end - start + 1, value); + end -= end - start + 1 - value.length(); + } + start = baseUrl.find('{', end + 1); + } + return baseUrl; +} + +void BannerSet::ReadBanners(istream & s) +{ + m_banners.clear(); + + Banner banner; + string type; + int lineNumber = 1; + for (string line; getline(s, line); ++lineNumber) + { + strings::Trim(line); + if (line.empty() || line.front() == '#') + continue; + + auto const equals = line.find('='); + if (equals == string::npos) + { + // Section header, should be in square brackets. + CHECK(line.front() == '[' && line.back() == ']', ("Unknown syntax at line", lineNumber)); + strings::Trim(line, " \t[]"); + CHECK(!line.empty(), ("Empty banner ID at line", lineNumber)); + if (!banner.IsEmpty()) + Add(banner, type); + banner = Banner(line); + type = "sponsored-banner-" + line; + } + else + { + // Variable definition, must be inside a section. + CHECK(!banner.IsEmpty(), ("Variable definition outside a section at line", lineNumber)); + string name = line.substr(0, equals); + string value = line.substr(equals + 1); + strings::Trim(name); + CHECK(!name.empty(), ("Empty variable name at line", lineNumber)); + strings::Trim(value); + if (name == "type") + type = value; + else + banner.SetProperty(name, value); + } + } + if (!banner.IsEmpty()) + Add(banner, type); +} + +void BannerSet::Add(Banner const & banner, string const & type) +{ + vector<string> v; + strings::Tokenize(type, "-", MakeBackInsertFunctor(v)); + uint32_t const ctype = classif().GetTypeByPathSafe(v); + if (ctype == 0) + { + LOG(LWARNING, ("Missing type", type, "for a banner")); + } + else + { + CHECK(m_banners.find(ctype) == m_banners.end(), ("Duplicate banner type", type)); + m_banners.emplace(make_pair(ctype, banner)); + } +} + +bool BannerSet::HasBannerForType(uint32_t type) const +{ + return m_banners.find(type) != m_banners.end(); +} + +Banner const & BannerSet::GetBannerForType(uint32_t type) const +{ + auto const result = m_banners.find(type); + CHECK(result != m_banners.end(), ("GetBannerForType() for absent banner")); + return result->second; +} + +bool BannerSet::HasBannerForFeature(FeatureType const & ft) const +{ + bool result = false; + ft.ForEachType([this, &result](uint32_t type) + { + if (!result && HasBannerForType(type)) + result = true; + }); + return result; +} + +Banner const & BannerSet::GetBannerForFeature(FeatureType const & ft) const +{ + vector<uint32_t> types; + ft.ForEachType([this, &types](uint32_t type) + { + if (types.empty() && HasBannerForType(type)) + types.push_back(type); + }); + CHECK(!types.empty(), ("No banners for the feature", ft)); + return GetBannerForType(types.front()); +} + +void BannerSet::LoadBanners() +{ + auto reader = GetPlatform().GetReader(BANNERS_FILE); + ReaderStreamBuf buffer(move(reader)); + istream s(&buffer); + ReadBanners(s); +} +} // namespace banner diff --git a/indexer/banners.hpp b/indexer/banners.hpp new file mode 100644 index 0000000000..9fb280e49a --- /dev/null +++ b/indexer/banners.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include "std/ctime.hpp" +#include "std/iostream.hpp" +#include "std/string.hpp" +#include "std/unordered_map.hpp" + +class FeatureType; + +namespace banner +{ +class Banner +{ +public: + Banner() = default; + explicit Banner(string const & id); + + bool IsEmpty() const { return m_id.empty(); } + string GetMessageBase() const { return m_messageBase; } + string GetIconName() const { return m_iconName; } + string GetDefaultUrl() const { return m_defaultUrl; } + bool IsActive() const; + + /// Replaces inline variables in the URL, uses the default banner URL if url is not specified. + string GetFormattedUrl(string const & url = {}) const; + + /// Usually called from BannerSet. + void SetProperty(string const & name, string const & value); + +private: + string m_id; + string m_messageBase; + string m_iconName; + string m_defaultUrl; + time_t m_activeAfter; + time_t m_activeBefore; + unordered_map<string, string> m_properties; + + string GetProperty(string const & name) const; +}; + +class BannerSet +{ +public: + void LoadBanners(); + void ReadBanners(istream & s); + + bool HasBannerForType(uint32_t type) const; + Banner const & GetBannerForType(uint32_t type) const; + + bool HasBannerForFeature(FeatureType const & ft) const; + Banner const & GetBannerForFeature(FeatureType const & ft) const; + +private: + unordered_map<uint32_t, Banner> m_banners; + + void Add(Banner const & banner, string const & type); +}; +} diff --git a/indexer/feature_meta.cpp b/indexer/feature_meta.cpp index 73e9451b2d..c1d34820fa 100644 --- a/indexer/feature_meta.cpp +++ b/indexer/feature_meta.cpp @@ -94,6 +94,8 @@ bool Metadata::TypeFromString(string const & k, Metadata::EType & outType) outType = Metadata::FMD_PRICE_RATE; else if (k == "rating:sponsored") outType = Metadata::FMD_RATING; + else if (k == "banner_url") + outType = Metadata::FMD_BANNER_URL; else return false; @@ -176,6 +178,7 @@ string DebugPrint(feature::Metadata::EType type) case Metadata::FMD_SPONSORED_ID: return "ref:sponsored"; case Metadata::FMD_PRICE_RATE: return "price_rate"; case Metadata::FMD_RATING: return "rating:sponsored"; + case Metadata::FMD_BANNER_URL: return "banner_url"; case Metadata::FMD_TEST_ID: return "test_id"; case Metadata::FMD_COUNT: CHECK(false, ("FMD_COUNT can not be used as a type.")); }; diff --git a/indexer/feature_meta.hpp b/indexer/feature_meta.hpp index 85cede6dc5..9b2a6862bd 100644 --- a/indexer/feature_meta.hpp +++ b/indexer/feature_meta.hpp @@ -126,6 +126,7 @@ public: FMD_SPONSORED_ID = 24, FMD_PRICE_RATE = 25, FMD_RATING = 26, + FMD_BANNER_URL = 27, FMD_COUNT }; diff --git a/indexer/indexer.pro b/indexer/indexer.pro index 57a5db455a..9b68c979fb 100644 --- a/indexer/indexer.pro +++ b/indexer/indexer.pro @@ -11,6 +11,7 @@ include($$ROOT_DIR/common.pri) SOURCES += \ altitude_loader.cpp \ + banners.cpp \ categories_holder.cpp \ categories_holder_loader.cpp \ categories_index.cpp \ @@ -64,6 +65,7 @@ SOURCES += \ HEADERS += \ altitude_loader.hpp \ + banners.hpp \ categories_holder.hpp \ categories_index.hpp \ cell_coverer.hpp \ diff --git a/indexer/indexer_tests/banners_test.cpp b/indexer/indexer_tests/banners_test.cpp new file mode 100644 index 0000000000..8fc773eeac --- /dev/null +++ b/indexer/indexer_tests/banners_test.cpp @@ -0,0 +1,70 @@ +#include "testing/testing.hpp" + +#include "indexer/banners.hpp" +#include "indexer/classificator.hpp" +#include "indexer/classificator_loader.hpp" + +#include "std/iostream.hpp" + +using namespace banner; + +UNIT_TEST(Banners_Load) +{ + char const kBanners[] = + "# comment\n" + "[abc]\n" + "icon = test.png\n" + "start=2016-07-14\n" + "type= shop-clothes \n" + "\n" + "[error]\n" + "[re_123]\n" + "type=shop-shoes\n" + "url=http://{aux}.com\n" + " \t aux=\t\ttest \n" + "[future]\n" + "type=shop-wine\n" + "start=2028-01-01\n" + "end=2028-12-31\n" + "[final]\n" + "type=shop-pet\n" + "start=2016-07-13\n" + "end=2016-07-14\n" + "\t"; + + classificator::Load(); + Classificator & c = classif(); + + BannerSet bs; + istringstream is(kBanners); + bs.ReadBanners(is); + + TEST(bs.HasBannerForType(c.GetTypeByPath({"shop", "clothes"})), ()); + Banner const & bannerAbc = bs.GetBannerForType(c.GetTypeByPath({"shop", "clothes"})); + TEST(!bannerAbc.IsEmpty(), ()); + TEST_EQUAL(bannerAbc.GetIconName(), "test.png", ()); + TEST_EQUAL(bannerAbc.GetMessageBase(), "banner_abc", ()); + TEST_EQUAL(bannerAbc.GetDefaultUrl(), "", ()); + TEST_EQUAL(bannerAbc.GetFormattedUrl("http://example.com"), "http://example.com", ()); + TEST_EQUAL(bannerAbc.GetFormattedUrl(), "", ()); + TEST(bannerAbc.IsActive(), ()); + + TEST(bs.HasBannerForType(c.GetTypeByPath({"shop", "shoes"})), ()); + Banner const & bannerRe = bs.GetBannerForType(c.GetTypeByPath({"shop", "shoes"})); + TEST(!bannerRe.IsEmpty(), ()); + TEST(bannerRe.IsActive(), ()); + TEST_EQUAL(bannerRe.GetIconName(), "banner_re_123.png", ()); + TEST_EQUAL(bannerRe.GetFormattedUrl(), "http://test.com", ()); + TEST_EQUAL(bannerRe.GetFormattedUrl("http://ex.ru/{aux}?var={v}"), "http://ex.ru/test?var={v}", ()); + + TEST(bs.HasBannerForType(c.GetTypeByPath({"shop", "wine"})), ()); + Banner const & bannerFuture = bs.GetBannerForType(c.GetTypeByPath({"shop", "wine"})); + TEST(!bannerFuture.IsEmpty(), ()); + TEST(!bannerFuture.IsActive(), ()); + + TEST(bs.HasBannerForType(c.GetTypeByPath({"shop", "pet"})), ()); + Banner const & bannerFinal = bs.GetBannerForType(c.GetTypeByPath({"shop", "pet"})); + TEST(!bannerFinal.IsEmpty(), ()); + TEST(!bannerFinal.IsActive(), ()); + TEST_EQUAL(bannerFinal.GetFormattedUrl("http://{aux}.ru"), "http://{aux}.ru", ()); +} diff --git a/indexer/indexer_tests/indexer_tests.pro b/indexer/indexer_tests/indexer_tests.pro index 1ff165f0ac..79b5de4b4a 100644 --- a/indexer/indexer_tests/indexer_tests.pro +++ b/indexer/indexer_tests/indexer_tests.pro @@ -28,6 +28,7 @@ HEADERS += \ SOURCES += \ ../../testing/testingmain.cpp \ + banners_test.cpp \ categories_test.cpp \ cell_coverer_test.cpp \ cell_id_test.cpp \ diff --git a/indexer/map_object.hpp b/indexer/map_object.hpp index e296e668b9..23c408b6ab 100644 --- a/indexer/map_object.hpp +++ b/indexer/map_object.hpp @@ -146,6 +146,7 @@ vector<Props> MetadataToProps(vector<T> const & metadata) case Metadata::FMD_SPONSORED_ID: case Metadata::FMD_PRICE_RATE: case Metadata::FMD_RATING: + case Metadata::FMD_BANNER_URL: case Metadata::FMD_TEST_ID: case Metadata::FMD_COUNT: break; |