Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSON support for changeset upload #407

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
325 changes: 325 additions & 0 deletions include/cgimap/api06/changeset_upload/osmchange_json_input_format.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
/**
* SPDX-License-Identifier: GPL-2.0-only
*
* This file is part of openstreetmap-cgimap (https://github.com/zerebubuth/openstreetmap-cgimap/).
*
* Copyright (C) 2009-2023 by the CGImap developer community.
* For a full list of authors see the git log.
*/

#ifndef OSMCHANGE_JSON_INPUT_FORMAT_HPP
#define OSMCHANGE_JSON_INPUT_FORMAT_HPP

#include "cgimap/api06/changeset_upload/node.hpp"
#include "cgimap/api06/changeset_upload/osmobject.hpp"
#include "cgimap/api06/changeset_upload/parser_callback.hpp"
#include "cgimap/api06/changeset_upload/relation.hpp"
#include "cgimap/api06/changeset_upload/way.hpp"
#include "cgimap/types.hpp"

#include "sjparser/sjparser.h"

#include <fmt/core.h>

#include <cassert>
#include <memory>
#include <string>
#include <utility>
#include <vector>


namespace api06 {

using SJParser::Array;
using SJParser::Member;
using SJParser::Object;
using SJParser::Parser;
using SJParser::Presence;
using SJParser::SArray;
using SJParser::SAutoObject;
using SJParser::SMap;
using SJParser::Value;
using SJParser::Reaction;
using SJParser::ObjectOptions;

using std::placeholders::_1;

class OSMChangeJSONParserFormat {

static auto getMemberParser() {
return SAutoObject{std::tuple{Member{"type", Value<std::string>{}},
Member{"ref", Value<int64_t>{}},
Member{"role", Value<std::string>{}, Presence::Optional, ""}},
ObjectOptions{Reaction::Ignore}
};
}

template <typename ElementParserCallback = std::nullptr_t>
static auto getElementsParser(ElementParserCallback element_parser_callback = nullptr) {
return Object{
std::tuple{
Member{"type", Value<std::string>{}},
Member{"action", Value<std::string>{}},
Member{"if-unused", Value<bool>{}, Presence::Optional, false},
Member{"id", Value<int64_t>{}},
Member{"lat", Value<double>{}, Presence::Optional},
Member{"lon", Value<double>{}, Presence::Optional},
Member{"version", Value<int64_t>{}, Presence::Optional},
Member{"changeset", Value<int64_t>{}},
Member{"tags", SMap{Value<std::string>{}}, Presence::Optional},
Member{"nodes", SArray{Value<int64_t>{}}, Presence::Optional},
Member{"members", SArray{getMemberParser()}, Presence::Optional}
},
ObjectOptions{Reaction::Ignore},
element_parser_callback};
}

template <typename ElementParserCallback = std::nullptr_t>
static auto getMainParser(ElementParserCallback element_parser_callback = nullptr) {
return Parser{
Object{
std::tuple{
Member{"version", Value<std::string>{}, Presence::Optional},
Member{"generator", Value<std::string>{}, Presence::Optional},
Member{"osmChange", Array{getElementsParser(element_parser_callback)}}
},ObjectOptions{Reaction::Ignore}}};
}

friend class OSMChangeJSONParser;
};

class OSMChangeJSONParser {

public:
explicit OSMChangeJSONParser(Parser_Callback& callback)
: m_callback(callback) { }

OSMChangeJSONParser(const OSMChangeJSONParser &) = delete;
OSMChangeJSONParser &operator=(const OSMChangeJSONParser &) = delete;

OSMChangeJSONParser(OSMChangeJSONParser &&) = delete;
OSMChangeJSONParser &operator=(OSMChangeJSONParser &&) = delete;

void process_message(const std::string &data) {

try {
m_callback.start_document();
_parser.parse(data);
_parser.finish();

if (_parser.parser().isEmpty()) {
throw payload_error("Empty JSON payload");
}

if (element_count == 0) {
throw payload_error("osmChange array is empty");
}

m_callback.end_document();
} catch (const std::exception& e) {
throw http::bad_request(e.what()); // rethrow JSON parser error as HTTP 400 Bad request
}
}

private:

using ElementsParser = decltype(api06::OSMChangeJSONParserFormat::getElementsParser());
using MainParser = decltype(api06::OSMChangeJSONParserFormat::getMainParser());

MainParser _parser{api06::OSMChangeJSONParserFormat::getMainParser(std::bind(&api06::OSMChangeJSONParser::process_element, this, _1))};

// OSM element callback
bool process_element(ElementsParser &parser) {

element_count++;

// process action
process_action(parser);

// process if-unused flag for delete action
process_if_unused(parser);

// process type (node, way, relation)
process_type(parser);

return true;
}

void process_action(ElementsParser &parser) {

const std::string& action = parser.get<1>();

if (action == "create") {
m_operation = operation::op_create;
} else if (action == "modify") {
m_operation = operation::op_modify;
} else if (action == "delete") {
m_operation = operation::op_delete;
} else {
throw payload_error{fmt::format("Unknown action {}, choices are create, modify, delete", action)};
}
}

void process_if_unused(ElementsParser &parser) {

if (m_operation == operation::op_delete) {
m_if_unused = false;
if (parser.parser<2>().isSet()) {
m_if_unused = parser.get<2>();
}
}
}

void process_type(ElementsParser &parser) {

const std::string& type = parser.get<0>();

if (type == "node") {
process_node(parser);
} else if (type == "way") {
process_way(parser);
} else if (type == "relation") {
process_relation(parser);
} else {
throw payload_error{fmt::format("Unknown element {}, expecting node, way or relation", type)};
}
}

void process_node(ElementsParser& parser) {

Node node;
init_object(node, parser);

if (parser.parser<4>().isSet()) {
node.set_lat(parser.get<4>());
}

if (parser.parser<5>().isSet()) {
node.set_lon(parser.get<5>());
}

process_tags(node, parser);

if (!node.is_valid(m_operation)) {
throw payload_error{fmt::format("{} does not include all mandatory fields", node.to_string())};
}

m_callback.process_node(node, m_operation, m_if_unused);
}

void process_way(ElementsParser& parser) {

Way way;
init_object(way, parser);

// adding way nodes
if (parser.parser<9>().isSet()) {
for (const auto& value : parser.get<9>()) {
way.add_way_node(value);
}
}

process_tags(way, parser);

if (!way.is_valid(m_operation)) {
throw payload_error{fmt::format("{} does not include all mandatory fields", way.to_string())};
}

m_callback.process_way(way, m_operation, m_if_unused);
}

void process_relation(ElementsParser& parser) {

Relation relation;
init_object(relation, parser);

process_relation_members(relation, parser);

process_tags(relation, parser);

if (!relation.is_valid(m_operation)) {
throw payload_error{fmt::format("{} does not include all mandatory fields", relation.to_string())};
}

m_callback.process_relation(relation, m_operation, m_if_unused);
}

void process_relation_members(Relation &relation, ElementsParser& parser) {

if (!parser.parser<10>().isSet()) {
return;
}

for (auto &mbr : parser.get<10>()) {
const auto& [type, ref, role] = mbr;

RelationMember member;
member.set_type(type);
member.set_ref(ref);
member.set_role(role);

if (!member.is_valid()) {
throw payload_error{fmt::format("Missing mandatory field on relation member in {}", relation.to_string()) };
}
relation.add_member(member);
}
}

void process_tags(OSMObject &o, ElementsParser& parser) {

if (parser.parser<8>().isSet()) {
for (const auto &tag : parser.get<8>()) {
o.add_tag(tag.first, tag.second);
}
}
}

void init_object(OSMObject &object, ElementsParser& parser) {

// id
object.set_id(parser.get<3>());

// version
if (parser.parser<6>().isSet()) {
object.set_version(parser.get<6>());
}

// changeset
if (parser.parser<7>().isSet()) {
object.set_changeset(parser.get<7>());
}

// TODO: not needed, handled by sjparser
if (!object.has_id()) {
throw payload_error{ "Mandatory field id missing in object" };
}

if (!object.has_changeset()) {
throw payload_error{fmt::format("Changeset id is missing for {}", object.to_string()) };
}

if (m_operation == operation::op_create) {
// we always override version number for create operations (they are not
// mandatory)
object.set_version(0u);
} else if (m_operation == operation::op_delete ||
m_operation == operation::op_modify) {
// objects for other operations must have a positive version number
if (!object.has_version()) {
throw payload_error{fmt::format("Version is required when updating {}", object.to_string()) };
}
if (object.version() < 1) {
throw payload_error{ fmt::format("Invalid version number {} in {}", object.version(), object.to_string()) };
}
}
}

operation m_operation = operation::op_undefined;
Parser_Callback& m_callback;
bool m_if_unused = false;
int element_count = 0;
};

} // namespace api06

#endif // OSMCHANGE_JSON_INPUT_FORMAT_HPP
3 changes: 3 additions & 0 deletions include/cgimap/json_formatter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class json_formatter : public output_formatter {
void start_changeset(bool) override;
void end_changeset(bool) override;

void start_diffresult() override;
void end_diffresult() override;
void start_action(action_type type) override;
void end_action(action_type type) override;
void error(const std::exception &e) override;
Expand All @@ -63,6 +65,7 @@ class json_formatter : public output_formatter {
const osm_nwr_signed_id_t old_id,
const osm_nwr_id_t new_id,
const osm_version_t new_version) override;

void write_diffresult_delete(const element_type elem,
const osm_nwr_signed_id_t old_id) override;

Expand Down
2 changes: 2 additions & 0 deletions include/cgimap/osm_diffresult_responder.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ class osm_diffresult_responder : public osm_responder {

~osm_diffresult_responder() override;

// lists the standard types that OSM format can respond in
std::vector<mime::type> types_available() const override;

void write(output_formatter& f,
const std::string &generator,
Expand Down
22 changes: 22 additions & 0 deletions include/cgimap/output_formatter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,23 @@ T element_type_name(element_type elt) noexcept {
return "";
}

template <typename T = const char*>
T action_type_name(action_type action) noexcept {

switch (action) {
case action_type::create:
return "create";
break;
case action_type::modify:
return "modify";
break;
case action_type::del:
return "delete";
break;
}
return "";
}

} // anonymous namespace

struct element_info {
Expand Down Expand Up @@ -216,6 +233,11 @@ struct output_formatter {

virtual void end_changeset(bool) = 0;

// marks the beginning of diffResult response processing
virtual void start_diffresult() = 0;

virtual void end_diffresult() = 0;

// TODO: document me.
virtual void start_action(action_type type) = 0;
virtual void end_action(action_type type) = 0;
Expand Down
2 changes: 2 additions & 0 deletions include/cgimap/text_formatter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ class text_formatter : public output_formatter {
void start_changeset(bool) override;
void end_changeset(bool) override;

void start_diffresult() override;
void end_diffresult() override;
void start_action(action_type type) override;
void end_action(action_type type) override;
void error(const std::exception &e) override;
Expand Down
Loading