[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:
Yingchun Lai
2020-07-16 22:39:51 +08:00
committed by GitHub
parent db50c19aad
commit d07a23ece3
11 changed files with 1133 additions and 51 deletions

View File

@ -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 */);

View File

@ -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

View File

@ -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;

View File

@ -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
View 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
View 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

View 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) << "&amp;";
break;
case '"': (*out) << "&quot;";
break;
case '\'': (*out) << "&apos;";
break;
case '<': (*out) << "&lt;";
break;
case '>': (*out) << "&gt;";
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);
}
}

View 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);
}

View File

@ -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)

View 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
View 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>