[webserver] Introduce mustache to simplify BE's website render (#4062)
cpp-mustache is a C++ implementation of a Mustache template engine with support for RapidJSON, and in order to simplify RapidJSON object building, we introduce class EasyJson from Apache Kudu.
This commit is contained in:
@ -104,7 +104,8 @@ void mem_usage_handler(MemTracker* mem_tracker, const WebPageHandler::ArgumentMa
|
||||
}
|
||||
|
||||
void add_default_path_handlers(WebPageHandler* web_page_handler, MemTracker* process_mem_tracker) {
|
||||
web_page_handler->register_page("/logs", "Logs", logs_handler, true /* is_on_nav_bar */);
|
||||
// TODO(yingchun): logs_handler is not implemented yet, so not show it on navigate bar
|
||||
web_page_handler->register_page("/logs", "Logs", logs_handler, false /* is_on_nav_bar */);
|
||||
web_page_handler->register_page("/varz", "Configs", config_handler, true /* is_on_nav_bar */);
|
||||
web_page_handler->register_page("/memz", "Memory",
|
||||
boost::bind<void>(&mem_usage_handler, process_mem_tracker, _1, _2), true /* is_on_nav_bar */);
|
||||
|
||||
@ -36,6 +36,7 @@
|
||||
#include "util/debug_util.h"
|
||||
#include "util/disk_info.h"
|
||||
#include "util/mem_info.h"
|
||||
#include "util/mustache/mustache.h"
|
||||
|
||||
using strings::Substitute;
|
||||
|
||||
@ -44,18 +45,32 @@ namespace doris {
|
||||
static std::string s_html_content_type = "text/html";
|
||||
|
||||
WebPageHandler::WebPageHandler(EvHttpServer* server) : _http_server(server) {
|
||||
_www_path = std::string(getenv("DORIS_HOME")) + "/www/";
|
||||
|
||||
// Make WebPageHandler to be static file handler, static files, e.g. css, png, will be handled by WebPageHandler.
|
||||
_http_server->register_static_file_handler(this);
|
||||
|
||||
PageHandlerCallback root_callback =
|
||||
TemplatePageHandlerCallback root_callback =
|
||||
boost::bind<void>(boost::mem_fn(&WebPageHandler::root_handler), this, _1, _2);
|
||||
register_page("/", "Home", root_callback, false /* is_on_nav_bar */);
|
||||
register_template_page("/", "Home", root_callback, false /* is_on_nav_bar */);
|
||||
}
|
||||
|
||||
WebPageHandler::~WebPageHandler() {
|
||||
STLDeleteValues(&_page_map);
|
||||
}
|
||||
|
||||
void WebPageHandler::register_template_page(const std::string& path, const string& alias,
|
||||
const TemplatePageHandlerCallback& callback, bool is_on_nav_bar) {
|
||||
// Relative path which will be used to find .mustache file in _www_path
|
||||
string render_path = (path == "/") ? "/home" : path;
|
||||
auto wrapped_cb = [=](const ArgumentMap& args, std::stringstream* output) {
|
||||
EasyJson ej;
|
||||
callback(args, &ej);
|
||||
render(render_path, ej, true /* is_styled */, output);
|
||||
};
|
||||
register_page(path, alias, wrapped_cb, is_on_nav_bar);
|
||||
}
|
||||
|
||||
void WebPageHandler::register_page(const std::string& path, const string& alias,
|
||||
const PageHandlerCallback& callback, bool is_on_nav_bar) {
|
||||
boost::mutex::scoped_lock lock(_map_lock);
|
||||
@ -79,7 +94,7 @@ void WebPageHandler::handle(HttpRequest* req) {
|
||||
|
||||
if (handler == nullptr) {
|
||||
// Try to handle static file request
|
||||
do_file_response(std::string(getenv("DORIS_HOME")) + "/www/" + req->raw_path(), req);
|
||||
do_file_response(_www_path + req->raw_path(), req);
|
||||
// Has replied in do_file_response, so we return here.
|
||||
return;
|
||||
}
|
||||
@ -90,24 +105,22 @@ void WebPageHandler::handle(HttpRequest* req) {
|
||||
bool use_style = (params.find("raw") == params.end());
|
||||
|
||||
std::stringstream content;
|
||||
// Append header
|
||||
if (use_style) {
|
||||
bootstrap_page_header(&content);
|
||||
}
|
||||
|
||||
// Append content
|
||||
handler->callback()(params, &content);
|
||||
|
||||
// Append footer
|
||||
std::string output;
|
||||
if (use_style) {
|
||||
bootstrap_page_footer(&content);
|
||||
std::stringstream oss;
|
||||
render_main_template(content.str(), &oss);
|
||||
output = oss.str();
|
||||
} else {
|
||||
output = content.str();
|
||||
}
|
||||
|
||||
req->add_output_header(HttpHeaders::CONTENT_TYPE, s_html_content_type.c_str());
|
||||
HttpChannel::send_reply(req, HttpStatus::OK, content.str());
|
||||
HttpChannel::send_reply(req, HttpStatus::OK, output);
|
||||
}
|
||||
|
||||
static const std::string PAGE_HEADER = R"(
|
||||
static const std::string kMainTemplate = R"(
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@ -122,9 +135,6 @@ static const std::string PAGE_HEADER = R"(
|
||||
<link href='/doris.css' rel='stylesheet' />
|
||||
</head>
|
||||
<body>
|
||||
)";
|
||||
|
||||
static const std::string NAVIGATION_BAR_PREFIX = R"(
|
||||
<nav class="navbar navbar-default">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
@ -134,49 +144,79 @@ static const std::string NAVIGATION_BAR_PREFIX = R"(
|
||||
</div>
|
||||
<div id="navbar" class="navbar-collapse collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
)";
|
||||
|
||||
static const std::string NAVIGATION_BAR_SUFFIX = R"(
|
||||
{{#path_handlers}}
|
||||
<li><a class="nav-link"href="{{path}}">{{alias}}</a></li>
|
||||
{{/path_handlers}}
|
||||
</ul>
|
||||
</div><!--/.nav-collapse -->
|
||||
</div><!--/.container-fluid -->
|
||||
</nav>
|
||||
)";
|
||||
|
||||
static const std::string PAGE_FOOTER = R"(
|
||||
{{^static_pages_available}}
|
||||
<div style="color: red">
|
||||
<strong>Static pages not available. Make sure ${DORIS_HOME}/www/ exists and contains web static files.</strong>
|
||||
</div>
|
||||
{{/static_pages_available}}
|
||||
{{{content}}}
|
||||
</div>
|
||||
{{#footer_html}}
|
||||
<footer class="footer"><div class="container text-muted">
|
||||
{{{.}}}
|
||||
</div></footer>
|
||||
{{/footer_html}}
|
||||
</body>
|
||||
</html>
|
||||
)";
|
||||
|
||||
void WebPageHandler::bootstrap_page_header(std::stringstream* output) {
|
||||
boost::mutex::scoped_lock lock(_map_lock);
|
||||
(*output) << PAGE_HEADER;
|
||||
(*output) << NAVIGATION_BAR_PREFIX;
|
||||
for (auto& iter : _page_map) {
|
||||
(*output) << "<li><a href=\"" << iter.first << "\">" << iter.first << "</a></li>";
|
||||
}
|
||||
(*output) << NAVIGATION_BAR_SUFFIX;
|
||||
std::string WebPageHandler::mustache_partial_tag(const std::string& path) const {
|
||||
return Substitute("{{> $0.mustache}}", path);
|
||||
}
|
||||
|
||||
void WebPageHandler::bootstrap_page_footer(std::stringstream* output) {
|
||||
(*output) << PAGE_FOOTER;
|
||||
bool WebPageHandler::static_pages_available() const {
|
||||
bool is_dir = false;
|
||||
return Env::Default()->is_directory(_www_path, &is_dir).ok() && is_dir;
|
||||
}
|
||||
|
||||
void WebPageHandler::root_handler(const ArgumentMap& args, std::stringstream* output) {
|
||||
// _path_handler_lock already held by MongooseCallback
|
||||
(*output) << "<h2>Version</h2>";
|
||||
(*output) << "<pre>" << get_version_string(false) << "</pre>" << std::endl;
|
||||
(*output) << "<h2>Hardware Info</h2>";
|
||||
(*output) << "<pre>";
|
||||
(*output) << CpuInfo::debug_string();
|
||||
(*output) << MemInfo::debug_string();
|
||||
(*output) << DiskInfo::debug_string();
|
||||
(*output) << "</pre>";
|
||||
|
||||
(*output) << "<h2>Status Pages</h2>";
|
||||
for (auto& iter : _page_map) {
|
||||
(*output) << "<a href=\"" << iter.first << "\">" << iter.first << "</a><br/>";
|
||||
bool WebPageHandler::mustache_template_available(const std::string& path) const {
|
||||
if (!static_pages_available()) {
|
||||
return false;
|
||||
}
|
||||
return Env::Default()->path_exists(Substitute("$0/$1.mustache", _www_path, path)).ok();
|
||||
}
|
||||
|
||||
void WebPageHandler::render_main_template(const std::string& content, std::stringstream* output) {
|
||||
static const std::string& footer = std::string("<pre>") + get_version_string(true) + std::string("</pre>");
|
||||
|
||||
EasyJson ej;
|
||||
ej["static_pages_available"] = static_pages_available();
|
||||
ej["content"] = content;
|
||||
ej["footer_html"] = footer;
|
||||
EasyJson path_handlers = ej.Set("path_handlers", EasyJson::kArray);
|
||||
for (const auto& handler : _page_map) {
|
||||
if (handler.second->is_on_nav_bar()) {
|
||||
EasyJson path_handler = path_handlers.PushBack(EasyJson::kObject);
|
||||
path_handler["path"] = handler.first;
|
||||
path_handler["alias"] = handler.second->alias();
|
||||
}
|
||||
}
|
||||
mustache::RenderTemplate(kMainTemplate, _www_path, ej.value(), output);
|
||||
}
|
||||
|
||||
void WebPageHandler::render(const string& path, const EasyJson& ej, bool use_style,
|
||||
std::stringstream* output) {
|
||||
if (mustache_template_available(path)) {
|
||||
mustache::RenderTemplate(mustache_partial_tag(path), _www_path, ej.value(), output);
|
||||
} else if (use_style) {
|
||||
(*output) << "<pre>" << ej.ToString() << "</pre>";
|
||||
} else {
|
||||
(*output) << ej.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
void WebPageHandler::root_handler(const ArgumentMap& args, EasyJson* output) {
|
||||
(*output)["version"] = get_version_string(false);
|
||||
(*output)["cpuinfo"] = CpuInfo::debug_string();
|
||||
(*output)["meminfo"] = MemInfo::debug_string();
|
||||
(*output)["diskinfo"] = DiskInfo::debug_string();
|
||||
}
|
||||
|
||||
} // namespace doris
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
#include <boost/thread/mutex.hpp>
|
||||
|
||||
#include "http/http_handler.h"
|
||||
#include "util/easy_json.h"
|
||||
|
||||
namespace doris {
|
||||
|
||||
@ -39,22 +40,46 @@ public:
|
||||
typedef std::map<std::string, std::string> ArgumentMap;
|
||||
typedef boost::function<void (const ArgumentMap& args, std::stringstream* output)>
|
||||
PageHandlerCallback;
|
||||
typedef boost::function<void (const ArgumentMap& args, EasyJson* output)>
|
||||
TemplatePageHandlerCallback;
|
||||
|
||||
WebPageHandler(EvHttpServer* http_server);
|
||||
virtual ~WebPageHandler();
|
||||
|
||||
void handle(HttpRequest *req) override;
|
||||
|
||||
// Register a route 'path'.
|
||||
// Register a route 'path' to be rendered via template.
|
||||
// The appropriate template to use is determined by 'path'.
|
||||
// If 'is_on_nav_bar' is true, a link to the page will be placed on the navbar
|
||||
// in the header of styled pages. The link text is given by 'alias'.
|
||||
void register_template_page(const std::string& path, const std::string& alias,
|
||||
const TemplatePageHandlerCallback& callback, bool is_on_nav_bar);
|
||||
|
||||
// Register a route 'path'. See the register_template_page for details.
|
||||
void register_page(const std::string& path, const std::string& alias,
|
||||
const PageHandlerCallback& callback, bool is_on_nav_bar);
|
||||
|
||||
private:
|
||||
void bootstrap_page_header(std::stringstream* output);
|
||||
void bootstrap_page_footer(std::stringstream* output);
|
||||
void root_handler(const ArgumentMap& args, std::stringstream* output);
|
||||
void root_handler(const ArgumentMap& args, EasyJson* output);
|
||||
|
||||
// Returns a mustache tag that renders the partial at path when
|
||||
// passed to mustache::RenderTemplate.
|
||||
std::string mustache_partial_tag(const std::string& path) const;
|
||||
|
||||
// Returns whether or not a mustache template corresponding
|
||||
// to the given path can be found.
|
||||
bool mustache_template_available(const std::string& path) const;
|
||||
|
||||
// Renders the main HTML template with the pre-rendered string 'content'
|
||||
// in the main body of the page, into 'output'.
|
||||
void render_main_template(const std::string& content, std::stringstream* output);
|
||||
|
||||
// Renders the template corresponding to 'path' (if available), using
|
||||
// fields in 'ej'.
|
||||
void render(const std::string& path, const EasyJson& ej, bool use_style,
|
||||
std::stringstream* output);
|
||||
|
||||
bool static_pages_available() const;
|
||||
|
||||
// Container class for a list of path handler callbacks for a single URL.
|
||||
class PathHandler {
|
||||
@ -85,6 +110,7 @@ private:
|
||||
PageHandlerCallback callback_;
|
||||
};
|
||||
|
||||
std::string _www_path;
|
||||
EvHttpServer* _http_server;
|
||||
// Lock guarding the _path_handlers map
|
||||
boost::mutex _map_lock;
|
||||
|
||||
@ -96,6 +96,8 @@ set(UTIL_FILES
|
||||
trace.cpp
|
||||
trace_metrics.cpp
|
||||
timezone_utils.cpp
|
||||
easy_json.cc
|
||||
mustache/mustache.cc
|
||||
)
|
||||
|
||||
if (WITH_MYSQL)
|
||||
|
||||
209
be/src/util/easy_json.cc
Normal file
209
be/src/util/easy_json.cc
Normal file
@ -0,0 +1,209 @@
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
#include "util/easy_json.h"
|
||||
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <glog/logging.h>
|
||||
#include <rapidjson/document.h>
|
||||
#include <rapidjson/rapidjson.h>
|
||||
#include <rapidjson/stringbuffer.h>
|
||||
#include <rapidjson/writer.h>
|
||||
// IWYU pragma: no_include <rapidjson/encodings.h>
|
||||
|
||||
using rapidjson::SizeType;
|
||||
using rapidjson::Value;
|
||||
using std::string;
|
||||
|
||||
namespace doris {
|
||||
|
||||
EasyJson::EasyJson() : alloc_(new EasyJsonAllocator), value_(&alloc_->value()) {}
|
||||
|
||||
EasyJson::EasyJson(EasyJson::ComplexTypeInitializer type)
|
||||
: alloc_(new EasyJsonAllocator), value_(&alloc_->value()) {
|
||||
if (type == kObject) {
|
||||
value_->SetObject();
|
||||
} else if (type == kArray) {
|
||||
value_->SetArray();
|
||||
}
|
||||
}
|
||||
|
||||
EasyJson EasyJson::Get(const string& key) {
|
||||
if (!value_->IsObject()) {
|
||||
value_->SetObject();
|
||||
}
|
||||
if (!value_->HasMember(key.c_str())) {
|
||||
Value key_val(key.c_str(), alloc_->allocator());
|
||||
value_->AddMember(key_val, Value().SetNull(), alloc_->allocator());
|
||||
}
|
||||
return EasyJson(&(*value_)[key.c_str()], alloc_);
|
||||
}
|
||||
|
||||
EasyJson EasyJson::Get(int index) {
|
||||
if (!value_->IsArray()) {
|
||||
value_->SetArray();
|
||||
}
|
||||
while (SizeType(index) >= value_->Size()) {
|
||||
value_->PushBack(Value().SetNull(), alloc_->allocator());
|
||||
}
|
||||
return EasyJson(&(*value_)[index], alloc_);
|
||||
}
|
||||
|
||||
EasyJson EasyJson::operator[](const string& key) {
|
||||
return Get(key);
|
||||
}
|
||||
|
||||
EasyJson EasyJson::operator[](int index) {
|
||||
return Get(index);
|
||||
}
|
||||
|
||||
EasyJson& EasyJson::operator=(const string& val) {
|
||||
value_->SetString(val.c_str(), alloc_->allocator());
|
||||
return *this;
|
||||
}
|
||||
template<typename T>
|
||||
EasyJson& EasyJson::operator=(T val) {
|
||||
*value_ = val;
|
||||
return *this;
|
||||
}
|
||||
template EasyJson& EasyJson::operator=(bool val);
|
||||
template EasyJson& EasyJson::operator=(int32_t val);
|
||||
template EasyJson& EasyJson::operator=(int64_t val);
|
||||
template EasyJson& EasyJson::operator=(uint32_t val);
|
||||
template EasyJson& EasyJson::operator=(uint64_t val);
|
||||
template EasyJson& EasyJson::operator=(double val);
|
||||
template<> EasyJson& EasyJson::operator=(const char* val) {
|
||||
value_->SetString(val, alloc_->allocator());
|
||||
return *this;
|
||||
}
|
||||
template<> EasyJson& EasyJson::operator=(EasyJson::ComplexTypeInitializer val) {
|
||||
if (val == kObject) {
|
||||
value_->SetObject();
|
||||
} else if (val == kArray) {
|
||||
value_->SetArray();
|
||||
}
|
||||
return (*this);
|
||||
}
|
||||
|
||||
EasyJson& EasyJson::SetObject() {
|
||||
if (!value_->IsObject()) {
|
||||
value_->SetObject();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
EasyJson& EasyJson::SetArray() {
|
||||
if (!value_->IsArray()) {
|
||||
value_->SetArray();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
EasyJson EasyJson::Set(const string& key, const string& val) {
|
||||
return (Get(key) = val);
|
||||
}
|
||||
template<typename T>
|
||||
EasyJson EasyJson::Set(const string& key, T val) {
|
||||
return (Get(key) = val);
|
||||
}
|
||||
template EasyJson EasyJson::Set(const string& key, bool val);
|
||||
template EasyJson EasyJson::Set(const string& key, int32_t val);
|
||||
template EasyJson EasyJson::Set(const string& key, int64_t val);
|
||||
template EasyJson EasyJson::Set(const string& key, uint32_t val);
|
||||
template EasyJson EasyJson::Set(const string& key, uint64_t val);
|
||||
template EasyJson EasyJson::Set(const string& key, double val);
|
||||
template EasyJson EasyJson::Set(const string& key, const char* val);
|
||||
template EasyJson EasyJson::Set(const string& key,
|
||||
EasyJson::ComplexTypeInitializer val);
|
||||
|
||||
EasyJson EasyJson::Set(int index, const string& val) {
|
||||
return (Get(index) = val);
|
||||
}
|
||||
template<typename T>
|
||||
EasyJson EasyJson::Set(int index, T val) {
|
||||
return (Get(index) = val);
|
||||
}
|
||||
template EasyJson EasyJson::Set(int index, bool val);
|
||||
template EasyJson EasyJson::Set(int index, int32_t val);
|
||||
template EasyJson EasyJson::Set(int index, int64_t val);
|
||||
template EasyJson EasyJson::Set(int index, uint32_t val);
|
||||
template EasyJson EasyJson::Set(int index, uint64_t val);
|
||||
template EasyJson EasyJson::Set(int index, double val);
|
||||
template EasyJson EasyJson::Set(int index, const char* val);
|
||||
template EasyJson EasyJson::Set(int index,
|
||||
EasyJson::ComplexTypeInitializer val);
|
||||
|
||||
EasyJson EasyJson::PushBack(const string& val) {
|
||||
if (!value_->IsArray()) {
|
||||
value_->SetArray();
|
||||
}
|
||||
Value push_val(val.c_str(), alloc_->allocator());
|
||||
value_->PushBack(push_val, alloc_->allocator());
|
||||
return EasyJson(&(*value_)[value_->Size() - 1], alloc_);
|
||||
}
|
||||
template<typename T>
|
||||
EasyJson EasyJson::PushBack(T val) {
|
||||
if (!value_->IsArray()) {
|
||||
value_->SetArray();
|
||||
}
|
||||
value_->PushBack(val, alloc_->allocator());
|
||||
return EasyJson(&(*value_)[value_->Size() - 1], alloc_);
|
||||
}
|
||||
template EasyJson EasyJson::PushBack(bool val);
|
||||
template EasyJson EasyJson::PushBack(int32_t val);
|
||||
template EasyJson EasyJson::PushBack(int64_t val);
|
||||
template EasyJson EasyJson::PushBack(uint32_t val);
|
||||
template EasyJson EasyJson::PushBack(uint64_t val);
|
||||
template EasyJson EasyJson::PushBack(double val);
|
||||
template<> EasyJson EasyJson::PushBack(const char* val) {
|
||||
if (!value_->IsArray()) {
|
||||
value_->SetArray();
|
||||
}
|
||||
Value push_val(val, alloc_->allocator());
|
||||
value_->PushBack(push_val, alloc_->allocator());
|
||||
return EasyJson(&(*value_)[value_->Size() - 1], alloc_);
|
||||
}
|
||||
template<> EasyJson EasyJson::PushBack(EasyJson::ComplexTypeInitializer val) {
|
||||
if (!value_->IsArray()) {
|
||||
value_->SetArray();
|
||||
}
|
||||
Value push_val;
|
||||
if (val == kObject) {
|
||||
push_val.SetObject();
|
||||
} else if (val == kArray) {
|
||||
push_val.SetArray();
|
||||
} else {
|
||||
LOG(FATAL) << "Unknown initializer type";
|
||||
}
|
||||
value_->PushBack(push_val, alloc_->allocator());
|
||||
return EasyJson(&(*value_)[value_->Size() - 1], alloc_);
|
||||
}
|
||||
|
||||
string EasyJson::ToString() const {
|
||||
rapidjson::StringBuffer buffer;
|
||||
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
|
||||
value_->Accept(writer);
|
||||
return buffer.GetString();
|
||||
}
|
||||
|
||||
EasyJson::EasyJson(Value* value, scoped_refptr<EasyJsonAllocator> alloc)
|
||||
: alloc_(std::move(alloc)), value_(value) {}
|
||||
|
||||
} // namespace doris
|
||||
190
be/src/util/easy_json.h
Normal file
190
be/src/util/easy_json.h
Normal file
@ -0,0 +1,190 @@
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <rapidjson/document.h>
|
||||
|
||||
#include "gutil/ref_counted.h"
|
||||
|
||||
namespace doris {
|
||||
|
||||
// A wrapper around rapidjson Value objects, to simplify usage.
|
||||
// Intended solely for building json objects, not writing/parsing.
|
||||
//
|
||||
// Simplifies code like this:
|
||||
//
|
||||
// rapidjson::Document d;
|
||||
// rapidjson::Value v;
|
||||
// v.SetObject();
|
||||
// rapidjson::Value list;
|
||||
// list.SetArray();
|
||||
// v.AddMember("list", list, d.GetAllocator());
|
||||
// v["list"].PushBack(rapidjson::Value().SetString("element"), d.GetAllocator());
|
||||
//
|
||||
// To this:
|
||||
//
|
||||
// EasyJson ej;
|
||||
// ej["list"][0] = "element";
|
||||
//
|
||||
// Client code should build objects as demonstrated above,
|
||||
// then call EasyJson::value() to obtain a reference to the
|
||||
// built rapidjson Value.
|
||||
class EasyJson {
|
||||
public:
|
||||
// Used for initializing EasyJson's with complex types.
|
||||
// For example:
|
||||
//
|
||||
// EasyJson array;
|
||||
// EasyJson nested = array.PushBack(EasyJson::kObject);
|
||||
// nested["attr"] = "val";
|
||||
// // array = [ { "attr": "val" } ]
|
||||
enum ComplexTypeInitializer {
|
||||
kObject,
|
||||
kArray
|
||||
};
|
||||
|
||||
EasyJson();
|
||||
// Initializes the EasyJson object with the given type.
|
||||
explicit EasyJson(ComplexTypeInitializer type);
|
||||
~EasyJson() = default;
|
||||
|
||||
// Returns the child EasyJson associated with key.
|
||||
//
|
||||
// Note: this method can mutate the EasyJson object
|
||||
// as follows:
|
||||
//
|
||||
// If this EasyJson's underlying Value is not an object
|
||||
// (i.e. !this->value().IsObject()), then its Value is
|
||||
// coerced to an object, overwriting the old Value.
|
||||
// If the given key does not exist, a Null-valued
|
||||
// EasyJson associated with key is created.
|
||||
EasyJson Get(const std::string& key);
|
||||
|
||||
// Returns the child EasyJson at index.
|
||||
//
|
||||
// Note: this method can mutate the EasyJson object
|
||||
// as follows:
|
||||
//
|
||||
// If this EasyJson's underlying Value is not an array
|
||||
// (i.e. !this->value().IsArray()), then its Value is
|
||||
// coerced to an array, overwriting the old Value.
|
||||
// If index >= this->value().Size(), then the underlying
|
||||
// array's size is increased to index + 1 (new indices
|
||||
// are filled with Null values).
|
||||
EasyJson Get(int index);
|
||||
|
||||
// Same as Get(key).
|
||||
EasyJson operator[](const std::string& key);
|
||||
// Same as Get(index).
|
||||
EasyJson operator[](int index);
|
||||
|
||||
// Sets the underlying Value equal to val.
|
||||
// Returns a reference to the object itself.
|
||||
//
|
||||
// 'val' can be a bool, int32_t, int64_t, double,
|
||||
// char*, string, or ComplexTypeInitializer.
|
||||
EasyJson& operator=(const std::string& val);
|
||||
template<typename T>
|
||||
EasyJson& operator=(T val);
|
||||
|
||||
// Sets the underlying Value to an object.
|
||||
// Returns a reference to the object itself.
|
||||
//
|
||||
// i.e. after calling SetObject(),
|
||||
// value().IsObject() == true
|
||||
EasyJson& SetObject();
|
||||
// Sets the underlying Value to an array.
|
||||
// Returns a reference to the object itself.
|
||||
//
|
||||
// i.e. after calling SetArray(),
|
||||
// value().IsArray() == true
|
||||
EasyJson& SetArray();
|
||||
|
||||
// Associates val with key.
|
||||
// Returns the child object.
|
||||
//
|
||||
// If this EasyJson's underlying Value is not an object
|
||||
// (i.e. !this->value().IsObject()), then its Value is
|
||||
// coerced to an object, overwriting the old Value.
|
||||
// If the given key does not exist, a new child entry
|
||||
// is created with the given value.
|
||||
EasyJson Set(const std::string& key, const std::string& val);
|
||||
template<typename T>
|
||||
EasyJson Set(const std::string& key, T val);
|
||||
|
||||
// Stores val at index.
|
||||
// Returns the child object.
|
||||
//
|
||||
// If this EasyJson's underlying Value is not an array
|
||||
// (i.e. !this->value().IsArray()), then its Value is
|
||||
// coerced to an array, overwriting the old Value.
|
||||
// If index >= this->value().Size(), then the underlying
|
||||
// array's size is increased to index + 1 (new indices
|
||||
// are filled with Null values).
|
||||
EasyJson Set(int index, const std::string& val);
|
||||
template<typename T>
|
||||
EasyJson Set(int index, T val);
|
||||
|
||||
// Appends val to the underlying array.
|
||||
// Returns a reference to the new child object.
|
||||
//
|
||||
// If this EasyJson's underlying Value is not an array
|
||||
// (i.e. !this->value().IsArray()), then its Value is
|
||||
// coerced to an array, overwriting the old Value.
|
||||
EasyJson PushBack(const std::string& val);
|
||||
template<typename T>
|
||||
EasyJson PushBack(T val);
|
||||
|
||||
// Returns a reference to the underlying Value.
|
||||
rapidjson::Value& value() const { return *value_; }
|
||||
|
||||
// Returns a string representation of the underlying json.
|
||||
std::string ToString() const;
|
||||
|
||||
private:
|
||||
// One instance of EasyJsonAllocator is shared among a root
|
||||
// EasyJson object and all of its descendants. The allocator
|
||||
// owns the underlying rapidjson Value, and a rapidjson
|
||||
// allocator (via a rapidjson::Document).
|
||||
class EasyJsonAllocator : public RefCounted<EasyJsonAllocator> {
|
||||
public:
|
||||
rapidjson::Value& value() { return value_; }
|
||||
rapidjson::Document::AllocatorType& allocator() { return value_.GetAllocator(); }
|
||||
private:
|
||||
friend class RefCounted<EasyJsonAllocator>;
|
||||
~EasyJsonAllocator() = default;
|
||||
|
||||
// The underlying rapidjson::Value object (Document is
|
||||
// a subclass of Value that has its own allocator).
|
||||
rapidjson::Document value_;
|
||||
};
|
||||
|
||||
// Used to instantiate descendant objects.
|
||||
EasyJson(rapidjson::Value* value, scoped_refptr<EasyJsonAllocator> alloc);
|
||||
|
||||
// One allocator is shared among an EasyJson object and
|
||||
// all of its descendants.
|
||||
scoped_refptr<EasyJsonAllocator> alloc_;
|
||||
|
||||
// A pointer to the underlying Value in the object
|
||||
// tree owned by alloc_.
|
||||
rapidjson::Value* value_;
|
||||
};
|
||||
|
||||
} // namespace doris
|
||||
448
be/src/util/mustache/mustache.cc
Normal file
448
be/src/util/mustache/mustache.cc
Normal file
@ -0,0 +1,448 @@
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "mustache.h"
|
||||
|
||||
#include "rapidjson/stringbuffer.h"
|
||||
#include <rapidjson/prettywriter.h>
|
||||
#include "rapidjson/writer.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
#include <stack>
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
using namespace rapidjson;
|
||||
using namespace std;
|
||||
using namespace boost::algorithm;
|
||||
|
||||
namespace mustache {
|
||||
|
||||
// TODO:
|
||||
// # Handle malformed templates better
|
||||
// # Better support for reading templates from files
|
||||
|
||||
enum TagOperator {
|
||||
SUBSTITUTION,
|
||||
SECTION_START,
|
||||
NEGATED_SECTION_START,
|
||||
PREDICATE_SECTION_START,
|
||||
SECTION_END,
|
||||
PARTIAL,
|
||||
COMMENT,
|
||||
LENGTH,
|
||||
EQUALITY,
|
||||
INEQUALITY,
|
||||
LITERAL,
|
||||
NONE
|
||||
};
|
||||
|
||||
struct OpCtx {
|
||||
TagOperator op;
|
||||
string tag_name;
|
||||
string tag_arg;
|
||||
bool escaped = false;
|
||||
};
|
||||
|
||||
struct ContextStack {
|
||||
const Value* value;
|
||||
const ContextStack* parent;
|
||||
};
|
||||
|
||||
TagOperator GetOperator(const string& tag) {
|
||||
if (tag.size() == 0) return SUBSTITUTION;
|
||||
switch (tag[0]) {
|
||||
case '#': return SECTION_START;
|
||||
case '^': return NEGATED_SECTION_START;
|
||||
case '?': return PREDICATE_SECTION_START;
|
||||
case '/': return SECTION_END;
|
||||
case '>': return PARTIAL;
|
||||
case '!':
|
||||
if (tag.size() == 1 || tag[1] != '=') return COMMENT;
|
||||
return INEQUALITY;
|
||||
case '%': return LENGTH;
|
||||
case '~': return LITERAL;
|
||||
case '=': return EQUALITY;
|
||||
default: return SUBSTITUTION;
|
||||
}
|
||||
}
|
||||
|
||||
int EvaluateTag(const string& document, const string& document_root, int idx,
|
||||
const ContextStack* context, const OpCtx& op_ctx, stringstream* out);
|
||||
|
||||
static bool RenderTemplate(const string& document, const string& document_root,
|
||||
const ContextStack* stack, stringstream* out);
|
||||
|
||||
void EscapeHtml(const string& in, stringstream *out) {
|
||||
for (const char& c: in) {
|
||||
switch (c) {
|
||||
case '&': (*out) << "&";
|
||||
break;
|
||||
case '"': (*out) << """;
|
||||
break;
|
||||
case '\'': (*out) << "'";
|
||||
break;
|
||||
case '<': (*out) << "<";
|
||||
break;
|
||||
case '>': (*out) << ">";
|
||||
break;
|
||||
default: (*out) << c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Dump(const rapidjson::Value& v) {
|
||||
StringBuffer buffer;
|
||||
Writer<StringBuffer> writer(buffer);
|
||||
v.Accept(writer);
|
||||
std::cout << buffer.GetString() << std::endl;
|
||||
}
|
||||
|
||||
// Breaks a dotted path into individual components. One wrinkle, which stops this from
|
||||
// being a simple split() is that we allow path components to be quoted, e.g.: "foo".bar,
|
||||
// and any '.' characters inside those quoted sections aren't considered to be
|
||||
// delimiters. This is to allow Json keys that contain periods.
|
||||
void FindJsonPathComponents(const string& path, vector<string>* components) {
|
||||
bool in_quote = false;
|
||||
bool escape_this_char = false;
|
||||
int start = 0;
|
||||
for (int i = start; i < path.size(); ++i) {
|
||||
if (path[i] == '"' && !escape_this_char) in_quote = !in_quote;
|
||||
if (path[i] == '.' && !escape_this_char && !in_quote) {
|
||||
// Current char == delimiter and not escaped and not in a quote pair => found a
|
||||
// component
|
||||
if (i - start > 0) {
|
||||
if (path[start] == '"' && path[(i - 1) - start] == '"') {
|
||||
if (i - start > 3) {
|
||||
components->push_back(path.substr(start + 1, i - (start + 2)));
|
||||
}
|
||||
} else {
|
||||
components->push_back(path.substr(start, i - start));
|
||||
}
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
escape_this_char = (path[i] == '\\' && !escape_this_char);
|
||||
}
|
||||
|
||||
if (path.size() - start > 0) {
|
||||
if (path[start] == '"' && path[(path.size() - 1) - start] == '"') {
|
||||
if (path.size() - start > 3) {
|
||||
components->push_back(path.substr(start + 1, path.size() - (start + 2)));
|
||||
}
|
||||
} else {
|
||||
components->push_back(path.substr(start, path.size() - start));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Looks up the json entity at 'path' in 'parent_context', and places it in 'resolved'. If
|
||||
// the entity does not exist (i.e. the path is invalid), 'resolved' will be set to nullptr.
|
||||
void ResolveJsonContext(const string& path, const ContextStack* stack,
|
||||
const Value** resolved) {
|
||||
if (path == ".") {
|
||||
*resolved = stack->value;
|
||||
return;
|
||||
}
|
||||
vector<string> components;
|
||||
FindJsonPathComponents(path, &components);
|
||||
|
||||
// At each enclosing level of context, try to resolve the path.
|
||||
for ( ; stack != nullptr; stack = stack->parent) {
|
||||
const Value* cur = stack->value;
|
||||
bool match = true;
|
||||
for(const string& c: components) {
|
||||
if (cur->IsObject() && cur->HasMember(c.c_str())) {
|
||||
cur = &(*cur)[c.c_str()];
|
||||
} else {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
*resolved = cur;
|
||||
return;
|
||||
}
|
||||
}
|
||||
*resolved = nullptr;
|
||||
}
|
||||
|
||||
int FindNextTag(const string& document, int idx, OpCtx* op, stringstream* out) {
|
||||
op->op = NONE;
|
||||
while (idx < document.size()) {
|
||||
if (document[idx] == '{' && idx < (document.size() - 3) && document[idx + 1] == '{') {
|
||||
if (document[idx + 2] == '{') {
|
||||
idx += 3;
|
||||
op->escaped = true;
|
||||
} else {
|
||||
op->escaped = false;
|
||||
idx += 2; // Now at start of template expression
|
||||
}
|
||||
stringstream expr;
|
||||
while (idx < document.size()) {
|
||||
if (document[idx] != '}') {
|
||||
expr << document[idx];
|
||||
++idx;
|
||||
} else {
|
||||
if (!op->escaped && idx < document.size() - 1 && document[idx + 1] == '}') {
|
||||
++idx;
|
||||
break;
|
||||
} else if (op->escaped && idx < document.size() - 2 && document[idx + 1] == '}'
|
||||
&& document[idx + 2] == '}') {
|
||||
idx += 2;
|
||||
break;
|
||||
} else {
|
||||
expr << '}';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string key = expr.str();
|
||||
trim(key);
|
||||
if (key != ".") trim_if(key, is_any_of("."));
|
||||
if (key.size() == 0) continue;
|
||||
op->op = GetOperator(key);
|
||||
if (op->op != SUBSTITUTION) {
|
||||
int len = op->op == INEQUALITY ? 2 : 1;
|
||||
key = key.substr(len);
|
||||
trim(key);
|
||||
}
|
||||
if (key.size() == 0) continue;
|
||||
|
||||
if (op->op == EQUALITY || op->op == INEQUALITY) {
|
||||
// Find an argument
|
||||
vector<string> components;
|
||||
split(components, key, is_any_of(" "));
|
||||
key = components[0];
|
||||
components.erase(components.begin());
|
||||
op->tag_arg = join(components, " ");
|
||||
}
|
||||
|
||||
op->tag_name = key;
|
||||
return ++idx;
|
||||
} else {
|
||||
if (out != nullptr) (*out) << document[idx];
|
||||
}
|
||||
++idx;
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
// Evaluates a [PREDICATE_|NEGATED_]SECTION_START / SECTION_END pair by evaluating the tag
|
||||
// in 'parent_context'. False or non-existant values cause the entire section to be
|
||||
// skipped. True values cause the section to be evaluated as though it were a normal
|
||||
// section, but with the parent context being the root context for that section.
|
||||
//
|
||||
// If 'is_negation' is true, the behaviour is the opposite of the above: false values
|
||||
// cause the section to be normally evaluated etc.
|
||||
int EvaluateSection(const string& document, const string& document_root, int idx,
|
||||
const ContextStack* context_stack, const OpCtx& op_ctx, stringstream* out) {
|
||||
// Precondition: idx is the immediate next character after an opening {{ #tag_name }}
|
||||
const Value* context;
|
||||
ResolveJsonContext(op_ctx.tag_name, context_stack, &context);
|
||||
|
||||
// If we a) cannot resolve the context from the tag name or b) the context evaluates to
|
||||
// false, we should skip the contents of the template until a closing {{/tag_name}}.
|
||||
bool skip_contents = false;
|
||||
|
||||
if (op_ctx.op == NEGATED_SECTION_START || op_ctx.op == PREDICATE_SECTION_START ||
|
||||
op_ctx.op == SECTION_START) {
|
||||
skip_contents = (context == nullptr || context->IsFalse());
|
||||
|
||||
// If the tag is a negative block (i.e. {{^tag_name}}), do the opposite: if the
|
||||
// context exists and is true, skip the contents, else echo them.
|
||||
if (op_ctx.op == NEGATED_SECTION_START) {
|
||||
context = context_stack->value;
|
||||
skip_contents = !skip_contents;
|
||||
} else if (op_ctx.op == PREDICATE_SECTION_START) {
|
||||
context = context_stack->value;
|
||||
}
|
||||
} else if (op_ctx.op == INEQUALITY || op_ctx.op == EQUALITY) {
|
||||
skip_contents = (context == nullptr || !context->IsString() ||
|
||||
strcasecmp(context->GetString(), op_ctx.tag_arg.c_str()) != 0);
|
||||
if (op_ctx.op == INEQUALITY) skip_contents = !skip_contents;
|
||||
context = context_stack->value;
|
||||
}
|
||||
|
||||
vector<const Value*> values;
|
||||
if (!skip_contents && context != nullptr && context->IsArray()) {
|
||||
for (int i = 0; i < context->Size(); ++i) {
|
||||
values.push_back(&(*context)[i]);
|
||||
}
|
||||
} else {
|
||||
values.push_back(skip_contents ? nullptr : context);
|
||||
}
|
||||
if (values.size() == 0) {
|
||||
skip_contents = true;
|
||||
values.push_back(nullptr);
|
||||
}
|
||||
|
||||
int start_idx = idx;
|
||||
for(const Value* v: values) {
|
||||
idx = start_idx;
|
||||
stack<OpCtx> section_starts;
|
||||
section_starts.push(op_ctx);
|
||||
while (idx < document.size()) {
|
||||
OpCtx next_ctx;
|
||||
idx = FindNextTag(document, idx, &next_ctx, skip_contents ? nullptr : out);
|
||||
if (skip_contents && (next_ctx.op == SECTION_START ||
|
||||
next_ctx.op == PREDICATE_SECTION_START ||
|
||||
next_ctx.op == NEGATED_SECTION_START)) {
|
||||
section_starts.push(next_ctx);
|
||||
} else if (next_ctx.op == SECTION_END) {
|
||||
if (next_ctx.tag_name != section_starts.top().tag_name) return -1;
|
||||
section_starts.pop();
|
||||
}
|
||||
if (section_starts.empty()) break;
|
||||
|
||||
// Don't need to evaluate any templates if we're skipping the contents
|
||||
if (!skip_contents) {
|
||||
ContextStack new_context = { v, context_stack };
|
||||
idx = EvaluateTag(document, document_root, idx, &new_context, next_ctx, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
// Evaluates a SUBSTITUTION tag, by replacing its contents with the value of the tag's
|
||||
// name in 'parent_context'.
|
||||
int EvaluateSubstitution(const string& document, const int idx,
|
||||
const ContextStack* context_stack, const OpCtx& op_ctx, stringstream* out) {
|
||||
const Value* val;
|
||||
ResolveJsonContext(op_ctx.tag_name, context_stack, &val);
|
||||
if (val == nullptr) return idx;
|
||||
if (val->IsString()) {
|
||||
if (!op_ctx.escaped) {
|
||||
EscapeHtml(val->GetString(), out);
|
||||
} else {
|
||||
// TODO: Triple {{{ means don't escape
|
||||
(*out) << val->GetString();
|
||||
}
|
||||
} else if (val->IsInt64()) {
|
||||
(*out) << val->GetInt64();
|
||||
} else if (val->IsInt()) {
|
||||
(*out) << val->GetInt();
|
||||
} else if (val->IsDouble()) {
|
||||
(*out) << val->GetDouble();
|
||||
} else if (val->IsBool()) {
|
||||
(*out) << boolalpha << val->GetBool();
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
// Evaluates a LENGTH tag by replacing its contents with the type-dependent 'size' of the
|
||||
// value.
|
||||
int EvaluateLength(const string& document, const int idx, const ContextStack* context_stack,
|
||||
const string& tag_name, stringstream* out) {
|
||||
const Value* val;
|
||||
ResolveJsonContext(tag_name, context_stack, &val);
|
||||
if (val == nullptr) return idx;
|
||||
if (val->IsArray()) {
|
||||
(*out) << val->Size();
|
||||
} else if (val->IsString()) {
|
||||
(*out) << val->GetStringLength();
|
||||
};
|
||||
|
||||
return idx;
|
||||
}
|
||||
|
||||
int EvaluateLiteral(const string& document, const int idx, const ContextStack* context_stack,
|
||||
const string& tag_name, stringstream* out) {
|
||||
const Value* val;
|
||||
ResolveJsonContext(tag_name, context_stack, &val);
|
||||
if (val == nullptr) return idx;
|
||||
if (!val->IsArray() && !val->IsObject()) return idx;
|
||||
StringBuffer strbuf;
|
||||
PrettyWriter<StringBuffer> writer(strbuf);
|
||||
val->Accept(writer);
|
||||
(*out) << strbuf.GetString();
|
||||
return idx;
|
||||
}
|
||||
|
||||
// Evaluates a 'partial' template by reading it fully from disk, then rendering it
|
||||
// directly into the current output with the current context.
|
||||
//
|
||||
// TODO: This could obviously be more efficient (and there are lots of file accesses in a
|
||||
// long list context).
|
||||
void EvaluatePartial(const string& tag_name, const string& document_root,
|
||||
const ContextStack* stack, stringstream* out) {
|
||||
stringstream ss;
|
||||
ss << document_root << tag_name;
|
||||
ifstream tmpl(ss.str().c_str());
|
||||
if (!tmpl.is_open()) {
|
||||
ss << ".mustache";
|
||||
tmpl.open(ss.str().c_str());
|
||||
if (!tmpl.is_open()) return;
|
||||
}
|
||||
stringstream file_ss;
|
||||
file_ss << tmpl.rdbuf();
|
||||
RenderTemplate(file_ss.str(), document_root, stack, out);
|
||||
}
|
||||
|
||||
// Given a tag name, and its operator, evaluate the tag in the given context and write the
|
||||
// output to 'out'. The heavy-lifting is delegated to specific Evaluate*()
|
||||
// methods. Returns the new cursor position within 'document', or -1 on error.
|
||||
int EvaluateTag(const string& document, const string& document_root, int idx,
|
||||
const ContextStack* context, const OpCtx& op_ctx, stringstream* out) {
|
||||
if (idx == -1) return idx;
|
||||
switch (op_ctx.op) {
|
||||
case SECTION_START:
|
||||
case PREDICATE_SECTION_START:
|
||||
case NEGATED_SECTION_START:
|
||||
case EQUALITY:
|
||||
case INEQUALITY:
|
||||
return EvaluateSection(document, document_root, idx, context, op_ctx, out);
|
||||
case SUBSTITUTION:
|
||||
return EvaluateSubstitution(document, idx, context, op_ctx, out);
|
||||
case COMMENT:
|
||||
return idx; // Ignored
|
||||
case PARTIAL:
|
||||
EvaluatePartial(op_ctx.tag_name, document_root, context, out);
|
||||
return idx;
|
||||
case LENGTH:
|
||||
return EvaluateLength(document, idx, context, op_ctx.tag_name, out);
|
||||
case LITERAL:
|
||||
return EvaluateLiteral(document, idx, context, op_ctx.tag_name, out);
|
||||
case NONE:
|
||||
return idx; // No tag was found
|
||||
case SECTION_END:
|
||||
return idx;
|
||||
default:
|
||||
cout << "Unknown tag: " << op_ctx.op << endl;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
static bool RenderTemplate(const string& document, const string& document_root,
|
||||
const ContextStack* stack, stringstream* out) {
|
||||
int idx = 0;
|
||||
while (idx < document.size() && idx != -1) {
|
||||
OpCtx op;
|
||||
idx = FindNextTag(document, idx, &op, out);
|
||||
idx = EvaluateTag(document, document_root, idx, stack, op, out);
|
||||
}
|
||||
|
||||
return idx != -1;
|
||||
}
|
||||
|
||||
bool RenderTemplate(const string& document, const string& document_root,
|
||||
const Value& context, stringstream* out) {
|
||||
ContextStack stack = { &context, nullptr };
|
||||
return RenderTemplate(document, document_root, &stack, out);
|
||||
}
|
||||
|
||||
}
|
||||
27
be/src/util/mustache/mustache.h
Normal file
27
be/src/util/mustache/mustache.h
Normal file
@ -0,0 +1,27 @@
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "rapidjson/document.h"
|
||||
#include <sstream>
|
||||
|
||||
// Routines for rendering Mustache (http://mustache.github.io) templates with RapidJson
|
||||
// (https://code.google.com/p/rapidjson/) documents.
|
||||
namespace mustache {
|
||||
|
||||
// Render a template contained in 'document' with respect to the json context
|
||||
// 'context'. Alternately finds a tag and then evaluates it. Returns when an error is
|
||||
// signalled (TODO: probably doesn't work in all paths), and evaluates that tag. Output is
|
||||
// accumulated in 'out'.
|
||||
bool RenderTemplate(const std::string& document, const std::string& document_root,
|
||||
const rapidjson::Value& context, std::stringstream* out);
|
||||
|
||||
}
|
||||
@ -64,3 +64,4 @@ ADD_BE_TEST(scoped_cleanup_test)
|
||||
ADD_BE_TEST(thread_test)
|
||||
ADD_BE_TEST(threadpool_test)
|
||||
ADD_BE_TEST(trace_test)
|
||||
ADD_BE_TEST(easy_json-test)
|
||||
|
||||
110
be/test/util/easy_json-test.cpp
Normal file
110
be/test/util/easy_json-test.cpp
Normal file
@ -0,0 +1,110 @@
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <rapidjson/document.h>
|
||||
#include <rapidjson/rapidjson.h>
|
||||
|
||||
#include "gutil/integral_types.h"
|
||||
#include "util/easy_json.h"
|
||||
|
||||
using rapidjson::SizeType;
|
||||
using rapidjson::Value;
|
||||
using std::string;
|
||||
|
||||
namespace doris {
|
||||
|
||||
class EasyJsonTest: public ::testing::Test {};
|
||||
|
||||
TEST_F(EasyJsonTest, TestNull) {
|
||||
EasyJson ej;
|
||||
ASSERT_TRUE(ej.value().IsNull());
|
||||
}
|
||||
|
||||
TEST_F(EasyJsonTest, TestBasic) {
|
||||
EasyJson ej;
|
||||
ej.SetObject();
|
||||
ej.Set("1", true);
|
||||
ej.Set("2", kint32min);
|
||||
ej.Set("4", kint64min);
|
||||
ej.Set("6", 1.0);
|
||||
ej.Set("7", "string");
|
||||
|
||||
Value& v = ej.value();
|
||||
|
||||
ASSERT_EQ(v["1"].GetBool(), true);
|
||||
ASSERT_EQ(v["2"].GetInt(), kint32min);
|
||||
ASSERT_EQ(v["4"].GetInt64(), kint64min);
|
||||
ASSERT_EQ(v["6"].GetDouble(), 1.0);
|
||||
ASSERT_EQ(string(v["7"].GetString()), "string");
|
||||
}
|
||||
|
||||
TEST_F(EasyJsonTest, TestNested) {
|
||||
EasyJson ej;
|
||||
ej.SetObject();
|
||||
ej.Get("nested").SetObject();
|
||||
ej.Get("nested").Set("nested_attr", true);
|
||||
ASSERT_EQ(ej.value()["nested"]["nested_attr"].GetBool(), true);
|
||||
|
||||
ej.Get("nested_array").SetArray();
|
||||
ej.Get("nested_array").PushBack(1);
|
||||
ej.Get("nested_array").PushBack(2);
|
||||
ASSERT_EQ(ej.value()["nested_array"][SizeType(0)].GetInt(), 1);
|
||||
ASSERT_EQ(ej.value()["nested_array"][SizeType(1)].GetInt(), 2);
|
||||
}
|
||||
|
||||
TEST_F(EasyJsonTest, TestCompactSyntax) {
|
||||
EasyJson ej;
|
||||
ej["nested"]["nested_attr"] = true;
|
||||
ASSERT_EQ(ej.value()["nested"]["nested_attr"].GetBool(), true);
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
ej["nested_array"][i] = i + 1;
|
||||
}
|
||||
ASSERT_EQ(ej.value()["nested_array"][SizeType(0)].GetInt(), 1);
|
||||
ASSERT_EQ(ej.value()["nested_array"][SizeType(1)].GetInt(), 2);
|
||||
}
|
||||
|
||||
TEST_F(EasyJsonTest, TestComplexInitializer) {
|
||||
EasyJson ej;
|
||||
ej = EasyJson::kObject;
|
||||
ASSERT_TRUE(ej.value().IsObject());
|
||||
|
||||
EasyJson nested_arr = ej.Set("nested_arr", EasyJson::kArray);
|
||||
ASSERT_TRUE(nested_arr.value().IsArray());
|
||||
|
||||
EasyJson nested_obj = nested_arr.PushBack(EasyJson::kObject);
|
||||
ASSERT_TRUE(ej["nested_arr"][0].value().IsObject());
|
||||
}
|
||||
|
||||
TEST_F(EasyJsonTest, TestAllocatorLifetime) {
|
||||
EasyJson* root = new EasyJson;
|
||||
EasyJson child = (*root)["child"];
|
||||
delete root;
|
||||
|
||||
child["child_attr"] = 1;
|
||||
ASSERT_EQ(child.value()["child_attr"].GetInt(), 1);
|
||||
}
|
||||
} // namespace doris
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
|
||||
28
webroot/be/home.mustache
Normal file
28
webroot/be/home.mustache
Normal file
@ -0,0 +1,28 @@
|
||||
{{!
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
}}
|
||||
|
||||
<h2>Version Info</h2>
|
||||
<pre>{{version}}</pre>
|
||||
<h2>Hardware Info</h2>
|
||||
<h3>CPU Info</h3>
|
||||
<pre>{{cpuinfo}}</pre>
|
||||
<h3>Memory Info</h3>
|
||||
<pre>{{meminfo}}</pre>
|
||||
<h3>Disk Info</h3>
|
||||
<pre>{{diskinfo}}</pre>
|
||||
Reference in New Issue
Block a user