diff --git a/editor/editor.pro b/editor/editor.pro index f597714be0..bed077a591 100644 --- a/editor/editor.pro +++ b/editor/editor.pro @@ -10,10 +10,12 @@ include($$ROOT_DIR/common.pri) SOURCES += \ opening_hours_ui.cpp \ + server_api.cpp \ ui2oh.cpp \ xml_feature.cpp \ HEADERS += \ opening_hours_ui.hpp \ + server_api.hpp \ ui2oh.hpp \ xml_feature.hpp \ diff --git a/editor/editor_tests/editor_tests.pro b/editor/editor_tests/editor_tests.pro index aac55b6519..dc8f421d52 100644 --- a/editor/editor_tests/editor_tests.pro +++ b/editor/editor_tests/editor_tests.pro @@ -4,7 +4,7 @@ CONFIG -= app_bundle TEMPLATE = app ROOT_DIR = ../.. -DEPENDENCIES = editor geometry coding base opening_hours pugixml +DEPENDENCIES = editor geometry coding base stats_client opening_hours pugixml include($$ROOT_DIR/common.pri) @@ -15,5 +15,6 @@ HEADERS += \ SOURCES += \ $$ROOT_DIR/testing/testingmain.cpp \ opening_hours_ui_test.cpp \ + server_api_test.cpp \ xml_feature_test.cpp \ ui2oh_test.cpp \ diff --git a/editor/editor_tests/server_api_test.cpp b/editor/editor_tests/server_api_test.cpp new file mode 100644 index 0000000000..dbec0ad0fa --- /dev/null +++ b/editor/editor_tests/server_api_test.cpp @@ -0,0 +1,123 @@ +#include "testing/testing.hpp" + +#include "editor/server_api.hpp" + +#include "geometry/mercator.hpp" + +#include "3party/pugixml/src/pugixml.hpp" + +using osm::ServerApi06; +using namespace pugi; + +constexpr char const * kOsmDevServer = "http://master.apis.dev.openstreetmap.org"; +constexpr char const * kValidOsmUser = "MapsMeTestUser"; +constexpr char const * kInvalidOsmUser = "qwesdxzcgretwr"; +ServerApi06 const kApi(kValidOsmUser, "12345678", kOsmDevServer); + +UNIT_TEST(OSM_ServerAPI_CheckUserAndPassword) +{ + TEST(kApi.CheckUserAndPassword(), ()); + + my::LogLevelSuppressor s; + TEST(!ServerApi06(kInvalidOsmUser, "3345dfce2", kOsmDevServer).CheckUserAndPassword(), ()); +} + +UNIT_TEST(OSM_ServerAPI_HttpCodeForUrl) +{ + TEST_EQUAL(200, ServerApi06::HttpCodeForUrl(string(kOsmDevServer) + "/user/" + kValidOsmUser), ()); + TEST_EQUAL(404, ServerApi06::HttpCodeForUrl(string(kOsmDevServer) + "/user/" + kInvalidOsmUser), ()); +} + +namespace +{ +// id attribute is set to -1. +// version attribute is set to 1. +void GenerateNodeXml(double lat, double lon, ServerApi06::TKeyValueTags const & tags, xml_document & outNode) +{ + outNode.reset(); + xml_node node = outNode.append_child("osm").append_child("node"); + node.append_attribute("id") = -1; + node.append_attribute("version") = 1; + node.append_attribute("lat") = lat; + node.append_attribute("lon") = lon; + for (auto const & kv : tags) + { + xml_node tag = node.append_child("tag"); + tag.append_attribute("k").set_value(kv.first.c_str()); + tag.append_attribute("v").set_value(kv.second.c_str()); + } +} + +string XmlToString(xml_document const & doc) +{ + ostringstream stream; + doc.print(stream); + return stream.str(); +} + +// Replaces attribute for tag (creates attribute if it's not present). +template +bool SetAttributeForOsmNode(xml_document & doc, char const * attribute, TNewValue const & v) +{ + xml_node node = doc.find_node([](xml_node const & n) + { + return 0 == strncmp(n.name(), "node", sizeof("node")); + }); + if (node.empty()) + return false; + if (node.attribute(attribute).empty()) + node.append_attribute(attribute) = v; + else + node.attribute(attribute) = v; + return true; +} + +} // namespace + +UNIT_TEST(SetAttributeForOsmNode) +{ + xml_document doc; + doc.append_child("osm").append_child("node"); + + TEST(SetAttributeForOsmNode(doc, "Test", 123), ()); + TEST_EQUAL(123, doc.child("osm").child("node").attribute("Test").as_int(), ()); + + TEST(SetAttributeForOsmNode(doc, "Test", 321), ()); + TEST_EQUAL(321, doc.child("osm").child("node").attribute("Test").as_int(), ()); +} + +UNIT_TEST(OSM_ServerAPI_ChangesetActions) +{ + uint64_t changeSetId; + TEST(kApi.CreateChangeSet({{"created_by", "MAPS.ME Unit Test"}, {"comment", "For test purposes only."}}, changeSetId), ()); + + xml_document node; + GenerateNodeXml(11.11, 12.12, {{"testkey", "firstnode"}}, node); + TEST(SetAttributeForOsmNode(node, "changeset", changeSetId), ()); + + uint64_t nodeId; + TEST(kApi.CreateNode(XmlToString(node), nodeId), ()); + TEST(SetAttributeForOsmNode(node, "id", nodeId), ()); + + TEST(SetAttributeForOsmNode(node, "lat", 10.10), ()); + TEST(kApi.ModifyNode(XmlToString(node), nodeId), ()); + // After modification, node version has increased. + TEST(SetAttributeForOsmNode(node, "version", 2), ()); + + // To retrieve created node, changeset should be closed first. + TEST(kApi.CloseChangeSet(changeSetId), ()); + + TEST(kApi.CreateChangeSet({{"created_by", "MAPS.ME Unit Test"}, {"comment", "For test purposes only."}}, changeSetId), ()); + // New changeset has new id. + TEST(SetAttributeForOsmNode(node, "changeset", changeSetId), ()); + + string const serverReply = kApi.GetXmlNodeByLatLon(node.child("osm").child("node").attribute("lat").as_double(), + node.child("osm").child("node").attribute("lon").as_double()); + xml_document reply; + reply.load_string(serverReply.c_str()); + TEST_EQUAL(nodeId, reply.child("osm").child("node").attribute("id").as_ullong(), ()); + + TEST(ServerApi06::DeleteResult::ESuccessfullyDeleted == kApi.DeleteNode(XmlToString(node), nodeId), ()); + + TEST(kApi.CloseChangeSet(changeSetId), ()); +} diff --git a/editor/server_api.cpp b/editor/server_api.cpp new file mode 100644 index 0000000000..fc1c62f315 --- /dev/null +++ b/editor/server_api.cpp @@ -0,0 +1,180 @@ +#include "editor/server_api.hpp" + +#include "geometry/mercator.hpp" + +#include "base/logging.hpp" +#include "base/string_utils.hpp" + +#include "std/sstream.hpp" + +#include "3party/Alohalytics/src/http_client.h" + +using alohalytics::HTTPClientPlatformWrapper; + +namespace osm +{ + +namespace +{ +void PrintRequest(HTTPClientPlatformWrapper const & r) +{ + LOG(LINFO, ("HTTP", r.http_method(), r.url_requested(), "has finished with code", r.error_code(), + (r.was_redirected() ? ", was redirected to " + r.url_received() : ""), + "Server replied:\n", r.server_response())); +} +} // namespace + +ServerApi06::ServerApi06(string const & user, string const & password, string const & baseUrl) + : m_user(user), m_password(password), m_baseOsmServerUrl(baseUrl) +{ +} + +bool ServerApi06::CreateChangeSet(TKeyValueTags const & kvTags, uint64_t & outChangeSetId) const +{ + ostringstream stream; + stream << "\n" + "\n"; + for (auto const & tag : kvTags) + stream << " \n"; + stream << "\n" + "\n"; + + HTTPClientPlatformWrapper request(m_baseOsmServerUrl + "/api/0.6/changeset/create"); + bool const success = request.set_user_and_password(m_user, m_password) + .set_body_data(move(stream.str()), "", "PUT") + .RunHTTPRequest(); + if (success && request.error_code() == 200) + { + if (strings::to_uint64(request.server_response(), outChangeSetId)) + return true; + LOG(LWARNING, ("Can't parse changeset ID from server response.")); + } + else + LOG(LWARNING, ("CreateChangeSet request has failed.")); + + PrintRequest(request); + return false; +} + +bool ServerApi06::CreateNode(string const & nodeXml, uint64_t & outCreatedNodeId) const +{ + HTTPClientPlatformWrapper request(m_baseOsmServerUrl + "/api/0.6/node/create"); + bool const success = request.set_user_and_password(m_user, m_password) + .set_body_data(move(nodeXml), "", "PUT") + .RunHTTPRequest(); + if (success && request.error_code() == 200) + { + if (strings::to_uint64(request.server_response(), outCreatedNodeId)) + return true; + LOG(LWARNING, ("Can't parse created node ID from server response.")); + } + else + LOG(LWARNING, ("CreateNode request has failed.")); + + PrintRequest(request); + return false; +} + +bool ServerApi06::ModifyNode(string const & nodeXml, uint64_t nodeId) const +{ + HTTPClientPlatformWrapper request(m_baseOsmServerUrl + "/api/0.6/node/" + strings::to_string(nodeId)); + bool const success = request.set_user_and_password(m_user, m_password) + .set_body_data(move(nodeXml), "", "PUT") + .RunHTTPRequest(); + if (success && request.error_code() == 200) + return true; + + LOG(LWARNING, ("ModifyNode request has failed.")); + PrintRequest(request); + return false; +} + +ServerApi06::DeleteResult ServerApi06::DeleteNode(string const & nodeXml, uint64_t nodeId) const +{ + HTTPClientPlatformWrapper request(m_baseOsmServerUrl + "/api/0.6/node/" + strings::to_string(nodeId)); + bool const success = request.set_user_and_password(m_user, m_password) + .set_body_data(move(nodeXml), "", "DELETE") + .RunHTTPRequest(); + if (success) + { + switch (request.error_code()) + { + case 200: return DeleteResult::ESuccessfullyDeleted; + case 412: return DeleteResult::ECanNotBeDeleted; + } + } + + LOG(LWARNING, ("DeleteNode request has failed.")); + PrintRequest(request); + return DeleteResult::EFailed; +} + +bool ServerApi06::CloseChangeSet(uint64_t changesetId) const +{ + HTTPClientPlatformWrapper request(m_baseOsmServerUrl + "/api/0.6/changeset/" + + strings::to_string(changesetId) + "/close"); + bool const success = request.set_user_and_password(m_user, m_password) + .set_http_method("PUT") + .RunHTTPRequest(); + if (success && request.error_code() == 200) + return true; + + LOG(LWARNING, ("CloseChangeSet request has failed.")); + PrintRequest(request); + return false; +} + +bool ServerApi06::CheckUserAndPassword() const +{ + static constexpr char const * kAPIWritePermission = "allow_write_api"; + HTTPClientPlatformWrapper request(m_baseOsmServerUrl + "/api/0.6/permissions"); + bool const success = request.set_user_and_password(m_user, m_password).RunHTTPRequest(); + if (success && request.error_code() == 200 && + request.server_response().find(kAPIWritePermission) != string::npos) + return true; + + LOG(LWARNING, ("OSM user and/or password are invalid.")); + PrintRequest(request); + return false; +} + +int ServerApi06::HttpCodeForUrl(string const & url) +{ + HTTPClientPlatformWrapper request(url); + bool const success = request.RunHTTPRequest(); + int const httpCode = request.error_code(); + if (success) + return httpCode; + + return -1; +} + +string ServerApi06::GetXmlFeaturesInRect(m2::RectD const & latLonRect) const +{ + using strings::to_string_dac; + + // Digits After Comma. + static constexpr double const kDAC = 7; + m2::PointD const lb = latLonRect.LeftBottom(); + m2::PointD const rt = latLonRect.RightTop(); + string const url = m_baseOsmServerUrl + "/api/0.6/map?bbox=" + to_string_dac(lb.x, kDAC) + ',' + to_string_dac(lb.y, kDAC) + ',' + + to_string_dac(rt.x, kDAC) + ',' + to_string_dac(rt.y, kDAC); + HTTPClientPlatformWrapper request(url); + bool const success = request.set_user_and_password(m_user, m_password).RunHTTPRequest(); + if (success && request.error_code() == 200) + return request.server_response(); + + LOG(LWARNING, ("GetXmlFeaturesInRect request has failed.")); + PrintRequest(request); + return string(); +} + +string ServerApi06::GetXmlNodeByLatLon(double lat, double lon) const +{ + constexpr double const kInflateRadiusDegrees = 1.0e-6; //!< ~1 meter. + m2::RectD rect(lon, lat, lon, lat); + rect.Inflate(kInflateRadiusDegrees, kInflateRadiusDegrees); + return GetXmlFeaturesInRect(rect); +} + +} // namespace osm diff --git a/editor/server_api.hpp b/editor/server_api.hpp new file mode 100644 index 0000000000..f4a04842af --- /dev/null +++ b/editor/server_api.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include "geometry/rect2d.hpp" + +#include "std/map.hpp" +#include "std/string.hpp" + +namespace osm +{ + +/// All methods here are synchronous and need wrappers for async usage. +/// TODO(AlexZ): Rewrite ServerAPI interface to accept XMLFeature. +class ServerApi06 +{ +public: + // k= and v= tags used in OSM. + using TKeyValueTags = map; + + /// Some nodes can't be deleted if they are used in ways or relations. + enum class DeleteResult + { + ESuccessfullyDeleted, + EFailed, + ECanNotBeDeleted + }; + + ServerApi06(string const & user, string const & password, string const & baseUrl = "http://api.openstreetmap.org"); + /// @returns true if connection with OSM server was established, and user+password are valid. + bool CheckUserAndPassword() const; + /// @returns http server code for given url or negative value in case of error. + /// This function can be used to check if user did not confirm email validation link after registration. + /// For example, for http://www.openstreetmap.org/user/UserName 200 is returned if UserName was registered. + static int HttpCodeForUrl(string const & url); + + /// Please use at least created_by=* and comment=* tags. + bool CreateChangeSet(TKeyValueTags const & kvTags, uint64_t & outChangeSetId) const; + /// nodeXml should be wrapped into ... tags. + bool CreateNode(string const & nodeXml, uint64_t & outCreatedNodeId) const; + /// nodeXml should be wrapped into ... tags. + bool ModifyNode(string const & nodeXml, uint64_t nodeId) const; + /// nodeXml should be wrapped into ... tags. + DeleteResult DeleteNode(string const & nodeXml, uint64_t nodeId) const; + bool CloseChangeSet(uint64_t changesetId) const; + + /// @returns OSM xml string with features in the bounding box or empty string on error. + string GetXmlFeaturesInRect(m2::RectD const & latLonRect) const; + string GetXmlNodeByLatLon(double lat, double lon) const; + +private: + string m_user; + string m_password; + string m_baseOsmServerUrl; +}; + +} // namespace osm