You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
AutoConnect/src/AutoConnectAux.cpp

471 lines
16 KiB

/**
* Implementation of AutoConnectAux class.
* @file AutoConnectAuxBasisImpl.h
* @author hieromon@gmail.com
* @version 0.9.7
* @date 2018-11-17
* @copyright MIT license.
*/
#include "AutoConnect.h"
#include "AutoConnectAux.h"
#include "AutoConnectElement.h"
#include "AutoConnectElementBasisImpl.h"
#ifdef AUTOCONNECT_USE_JSON
#include "AutoConnectElementJsonImpl.h"
#endif
/**
* Template for auxiliary page composed with AutoConnectAux of user sketch.
* The structure of the auxiliary page depends on this template for
* the purpose to be incorporated into the AutoConnect Menu.
* The page element implemented by AutoConnectElement is placed at the
* position of {{AUX_ELEMENT}} token. This token is contained in a
* <div> block with a class defined in 'base-panel' and is held by a
* <form> element with an ID '_aux'.
* The JavaScript that named 'sa' at the end of the template determines
* the behavior of AutoConnectSubmit.
*/
const char AutoConnectAux::_PAGE_AUX[] PROGMEM = {
"{{HEAD}}"
"<title>{{AUX_TITLE}}</title>"
"<style type=\"text/css\">"
"{{CSS_BASE}}"
"{{CSS_UL}}"
"{{CSS_INPUT_BUTTON}}"
"{{CSS_INPUT_TEXT}}"
"{{CSS_LUXBAR}}"
"</style>"
"</head>"
"<body style=\"padding-top:58px;\">"
"<div class=\"container\">"
"{{MENU_PRE}}"
"{{MENU_AUX}}"
"{{MENU_POST}}"
"<div class=\"base-panel\"><div class=\"aux-page\">"
"<form id='_aux' method=\"post\" onsubmit=\"return false;\">"
"<ul class=\"noorder\">"
"{{AUX_ELEMENT}}"
"</ul>"
"</form>"
"</div></div>"
"</div>"
"<script>"
"function _sa(url) {"
"document.getElementById('_aux').action=url;"
"document.getElementById('_aux').submit();"
"}"
"</script>"
"</body>"
"</html>"
};
/**
* Destructs container of AutoConnectElement and release a unique
* pointer of AutoConnect instance.
*/
AutoConnectAux::~AutoConnectAux() {
_addonElm.clear();
_addonElm.swap(_addonElm);
if (_ac)
_ac.release();
}
/**
* Add an AutoConnectElement
* @param addon A reference of AutoConnectElement.
*/
void AutoConnectAux::add(AutoConnectElement& addon) {
_addonElm.push_back(addon);
AC_DBG("%s placed on %s\n", addon.name.length() ? addon.name.c_str() : "*noname", uri());
}
/**
* Add an AutoConnectElement vector container to the AutoConnectAux page.
* @param addons AutoConnectElementVT collection.
*/
void AutoConnectAux::add(AutoConnectElementVT addons) {
for (std::size_t n = 0; n < addons.size(); n++)
add(addons[n]);
}
/**
* Releases the AutoConnectElements with the specified name from
* the AutoConnectAux page. Releases all AutoConnectElements with
* the same name in AutoConnectAux.
* @param name
* @return true The specified AutoConnectElements have been released.
* @return false The specified AutoConnectElement not found in AutoConnectAux.
*/
bool AutoConnectAux::release(const String name) {
bool rc = false;
for (std::size_t n = 0; n < _addonElm.size(); n++) {
String elmName = _addonElm[n].get().name;
if (elmName == name) {
AC_DBG("%s release from %s\n", elmName.c_str(), uri());
_addonElm.erase(_addonElm.begin() + n);
rc = true;
}
}
return rc;
}
/**
* Concatenates subsequent AutoConnectAux pages starting from oneself
* to the chain list.
* AutoConnectAux is collected in the chain list and each object is
* chained by the "_next". AutoConnect follows the "_next" to manage
* auxiliary pages. The _concat function concatenates subsequent
* AutoConnectAuxs.
* @param aux A reference of AutoConnectAux.
*/
void AutoConnectAux::_concat(AutoConnectAux& aux) {
if (_next)
_next->_concat(aux);
else
_next.reset(&aux);
}
/**
* Register the AutoConnect that owns itself.
* AutoConenctAux needs to access the AutoConnect member. Also
* AutoConnectAux is cataloged by chain list. The _join function
* registers AutoConnect in the following AutoConnectAux chain list.
* @param ac A reference of AutoConnect.
*/
void AutoConnectAux::_join(AutoConnect& ac) {
_ac.reset(&ac);
// Chain to subsequent AutoConnectAux in the list.
if (_next)
_next->_join(ac);
}
/**
* Insert the token handler of PageBuilder. This handler inserts HTML
* elements generated by the whole AutoConnectElements to the auxiliary page.
* @param args A reference of PageArgument but unused.
* @return HTML string that should be inserted.
*/
const String AutoConnectAux::_insertElement(PageArgument& args) {
AC_UNUSED(args);
String body = String();
if (_handler)
if (_order & AC_EXIT_AHEAD) {
AC_DBG("CB %s\n", uri());
body += _handler(args);
}
for (std::size_t n = 0; n < _addonElm.size(); n++) {
AutoConnectElement& addon = _addonElm[n];
body += addon.toHTML();
}
if (_handler)
if (_order & AC_EXIT_LATER) {
AC_DBG("CB %s\n", uri());
body += _handler(args);
}
return body;
}
/**
* Generate an auxiliary page assembled with the AutoConnectElement.
* This function is the core procedure of AutoConnectAux, and uses
* PageBuilder from the _PAGE_AUX template to build an AutoConnect
* menu and insert HTML elements. A template of an auxiliary page is
* fixed and its structure inherits from the AutoConnect.
* @param uri An uri of the auxiliary page.
* @return A PageElement of auxiliary page.
*/
PageElement* AutoConnectAux::_setupPage(String uri) {
PageElement* elm = nullptr;
if (_ac) {
if (uri != String(_uri)) {
if (_next) {
elm = _next->_setupPage(uri);
}
} else {
AutoConnect* mother = _ac.get();
// Overwrite actual AutoConnectMenu title to the Aux. page title
if (_title.length())
mother->_menuTitle = _title;
elm = new PageElement();
// Construct the auxiliary page
elm->setMold(_PAGE_AUX);
elm->addToken(PSTR("HEAD"), std::bind(&AutoConnect::_token_HEAD, mother, std::placeholders::_1));
elm->addToken(PSTR("AUX_TITLE"), std::bind(&AutoConnectAux::_injectTitle, this, std::placeholders::_1));
elm->addToken(PSTR("CSS_BASE"), std::bind(&AutoConnect::_token_CSS_BASE, mother, std::placeholders::_1));
elm->addToken(PSTR("CSS_UL"), std::bind(&AutoConnect::_token_CSS_UL, mother, std::placeholders::_1));
elm->addToken(PSTR("CSS_INPUT_BUTTON"), std::bind(&AutoConnect::_token_CSS_INPUT_BUTTON, mother, std::placeholders::_1));
elm->addToken(PSTR("CSS_INPUT_TEXT"), std::bind(&AutoConnect::_token_CSS_INPUT_TEXT, mother, std::placeholders::_1));
elm->addToken(PSTR("CSS_LUXBAR"), std::bind(&AutoConnect::_token_CSS_LUXBAR, mother, std::placeholders::_1));
elm->addToken(PSTR("MENU_PRE"), std::bind(&AutoConnect::_token_MENU_PRE, mother, std::placeholders::_1));
elm->addToken(PSTR("MENU_AUX"), std::bind(&AutoConnect::_token_MENU_AUX, mother, std::placeholders::_1));
elm->addToken(PSTR("MENU_POST"), std::bind(&AutoConnect::_token_MENU_POST, mother, std::placeholders::_1));
elm->addToken(PSTR("AUX_ELEMENT"), std::bind(&AutoConnectAux::_insertElement, this, std::placeholders::_1));
}
}
return elm;
}
/**
* Inject the <li> element depending on the "luxbar-item" attribute
* for implementing the AutoConnect menu.
* @param args A reference of PageArgument but it's only used for
* interface alignment and is not actually used.
* @return A concatenated string of <li> elements for the menu item of
* AutoConnect.
*/
const String AutoConnectAux::_injectMenu(PageArgument& args) {
String menuItem;
if (_menu)
menuItem = String(FPSTR("<li class=\"luxbar-item\"><a href=\"")) + String(_uri) + String("\">") + _title + String(FPSTR("</a></li>"));
if (_next)
menuItem += _next->_injectMenu(args);
return menuItem;
}
#ifdef AUTOCONNECT_USE_JSON
/**
* Create an instance from the AutoConnectElement of the JSON object.
* @param json A reference of JSON
* @return A pointer of created AutoConnectElement instance.
*/
AutoConnectElement* AutoConnectAux::_createElement(const JsonObject& json) {
AutoConnectElement* elm = nullptr;
String type = json.get<String>(F(AUTOCONNECT_JSON_KEY_TYPE));
switch (_asElementType(type)) {
case AC_Element:
elm = new AutoConnectElement;
break;
case AC_Button: {
AutoConnectButton* cert_elm = new AutoConnectButton;
return reinterpret_cast<AutoConnectElement*>(cert_elm);
}
case AC_Checkbox: {
AutoConnectCheckbox* cert_elm = new AutoConnectCheckbox;
return reinterpret_cast<AutoConnectElement*>(cert_elm);
}
case AC_Input: {
AutoConnectInput* cert_elm = new AutoConnectInput;
return reinterpret_cast<AutoConnectElement*>(cert_elm);
}
case AC_Radio: {
AutoConnectRadio* cert_elm = new AutoConnectRadio;
return reinterpret_cast<AutoConnectElement*>(cert_elm);
}
case AC_Select: {
AutoConnectSelect* cert_elm = new AutoConnectSelect;
return reinterpret_cast<AutoConnectElement*>(cert_elm);
}
case AC_Submit: {
AutoConnectSubmit* cert_elm = new AutoConnectSubmit;
return reinterpret_cast<AutoConnectElement*>(cert_elm);
}
case AC_Text: {
AutoConnectText* cert_elm = new AutoConnectText;
return reinterpret_cast<AutoConnectElement*>(cert_elm);
}
}
return elm;
}
/**
* Constructs an AutoConnectAux instance by reading all the
* AutoConnectElements of the specified URI from the elements defined JSON.
* @param in AutoConnectAux element data which is described by JSON.
* @return true The element collection successfully loaded.
* @return false Invalid JSON data occurred.
*/
bool AutoConnectAux::load(const char* in) {
// DynamicJsonBuffer jsonBuffer();
const size_t bufferSize = 2*JSON_ARRAY_SIZE(2) + 3*JSON_ARRAY_SIZE(3) + JSON_ARRAY_SIZE(9) + JSON_OBJECT_SIZE(1) + 2*JSON_OBJECT_SIZE(3) + 9*JSON_OBJECT_SIZE(4) + JSON_OBJECT_SIZE(5) + JSON_OBJECT_SIZE(6) + 1080;
DynamicJsonBuffer jsonBuffer(bufferSize);
JsonObject& jb = jsonBuffer.parseObject(in);
return _load(jb);
}
bool AutoConnectAux::load(const __FlashStringHelper* in) {
// DynamicJsonBuffer jsonBuffer();
const size_t bufferSize = 2*JSON_ARRAY_SIZE(2) + 3*JSON_ARRAY_SIZE(3) + JSON_ARRAY_SIZE(9) + JSON_OBJECT_SIZE(1) + 2*JSON_OBJECT_SIZE(3) + 9*JSON_OBJECT_SIZE(4) + JSON_OBJECT_SIZE(5) + JSON_OBJECT_SIZE(6) + 1080;
DynamicJsonBuffer jsonBuffer(bufferSize);
JsonObject& jb = jsonBuffer.parseObject(in);
return _load(jb);
}
bool AutoConnectAux::load(Stream& in) {
// DynamicJsonBuffer jsonBuffer();
const size_t bufferSize = 2*JSON_ARRAY_SIZE(2) + 3*JSON_ARRAY_SIZE(3) + JSON_ARRAY_SIZE(9) + JSON_OBJECT_SIZE(1) + 2*JSON_OBJECT_SIZE(3) + 9*JSON_OBJECT_SIZE(4) + JSON_OBJECT_SIZE(5) + JSON_OBJECT_SIZE(6) + 1080;
DynamicJsonBuffer jsonBuffer(bufferSize);
JsonObject& jb = jsonBuffer.parseObject(in);
return _load(jb);
}
bool AutoConnectAux::_load(JsonObject& jb) {
if (!jb.success())
return false;
_title = jb.get<String>(F(AUTOCONNECT_JSON_KEY_TITLE));
_uriStr = jb.get<String>(F(AUTOCONNECT_JSON_KEY_URI));
_uri = _uriStr.c_str();
_menu = jb.get<bool>(F(AUTOCONNECT_JSON_KEY_MENU));
(void)_loadElement(jb, "*");
return true;
}
/**
* Load element specified by the name parameter from the stream
* described by JSON. Usually, the Stream is specified a storm file of
* SD or SPIFFS. The Stream must be opened before invoking the function.
* @param in Reference of the Stream which contains the parameter
* data described by JSON.
* @param name The element name to be loaded. '*'specifies that all
* elements are to be loaded.
* @return A reference of loaded AutoConnectElement instance.
*/
AutoConnectElement& AutoConnectAux::loadElement(const char* in, const String name) {
DynamicJsonBuffer jsonBuffer;
JsonObject& jb = jsonBuffer.parseObject(in);
return _loadElement(jb, name);
}
AutoConnectElement& AutoConnectAux::loadElement(const __FlashStringHelper* in, const String name) {
DynamicJsonBuffer jsonBuffer;
JsonObject& jb = jsonBuffer.parseObject(in);
return _loadElement(jb, name);
}
AutoConnectElement& AutoConnectAux::loadElement(Stream& in, const String name) {
DynamicJsonBuffer jsonBuffer;
JsonObject& jb = jsonBuffer.parseObject(in);
return _loadElement(jb, name);
}
AutoConnectElement& AutoConnectAux::_loadElement(JsonObject& jb, const String name) {
AutoConnectElement* auxElm = nullptr;
bool wc = name == "*";
if (!jb.success())
return _nullElement();
JsonArray& elements = jb[AUTOCONNECT_JSON_KEY_ELEMENT];
for (JsonObject& element : elements) {
String elmName = element.get<String>(F(AUTOCONNECT_JSON_KEY_NAME));
if (wc || name.equalsIgnoreCase(elmName)) {
// The specified element is defined in the JSON stream.
// Loads from JSON object.
auxElm = _getElement(elmName);
// The element is not created yet, create new one.
if (!auxElm) {
if ((auxElm = _createElement(element))) {
AC_DBG("%s<%d> of %s created\n", elmName.c_str(), (int)(auxElm->typeOf()), uri());
add(*auxElm); // Insert to AutoConnect
}
else {
AC_DBG("%s unknown element type\n", elmName.c_str());
continue;
}
}
if (auxElm->loadElement(element))
AC_DBG("%s<%d> of %s loaded\n", auxElm->name.c_str(), (int)auxElm->typeOf(), uri());
else {
// Element type mismatch
AC_DBG("Type of %s element mismatched\n", elmName.c_str());
continue;
}
}
}
return auxElm ? *auxElm : _nullElement();
}
/**
* Serialize the element to JSON and write it to the stream.
* @param out An output stream
* @param element A reference of the element to be output.
* @return Number of byte output
*/
size_t AutoConnectAux::saveElement(Stream& out, const AutoConnectElement& element) {
DynamicJsonBuffer jsonBuffer;
JsonObject& jb = jsonBuffer.parseObject(out);
if (!jb.success())
return 0;
JsonArray& aux = jb["aux"];
if (!aux.success())
return 0;
for (JsonObject& page : aux) {
if (page["aux"].as<String>() == String(uri())) {
JsonArray& element_j = page[AUTOCONNECT_JSON_KEY_ELEMENT];
for (JsonObject& elm : element_j) {
if (elm[AUTOCONNECT_JSON_KEY_NAME].as<String>() == element.name) {
elm.set(F(AUTOCONNECT_JSON_KEY_VALUE), element.value);
return jb.prettyPrintTo(out);
}
}
}
}
return 0;
}
/**
* Get already registered AutoConnectElement.
* @param name Element name
* @return A pointer to the registered AutoConnectElement.
*/
AutoConnectElement* AutoConnectAux::_getElement(const String name) {
for (std::size_t n = 0; n < _addonElm.size(); n++)
if (_addonElm[n].get().name == name)
return &(_addonElm[n].get());
return nullptr;
}
/**
* Convert element type from type as String.
* @param type An element type as String
* @return A type of ACElement_t
*/
const ACElement_t AutoConnectAux::_asElementType(const String type) {
typedef struct {
const char* tName;
ACElement_t tEnum;
} ACElementType_t;
static const ACElementType_t types[] PROGMEM = {
{ AUTOCONNECT_JSON_TYPE_ACBUTTON, AC_Button },
{ AUTOCONNECT_JSON_TYPE_ACCHECKBOX, AC_Checkbox },
{ AUTOCONNECT_JSON_TYPE_ACELEMENT, AC_Element },
{ AUTOCONNECT_JSON_TYPE_ACINPUT, AC_Input },
{ AUTOCONNECT_JSON_TYPE_ACRADIO, AC_Radio },
{ AUTOCONNECT_JSON_TYPE_ACSELECT, AC_Select },
{ AUTOCONNECT_JSON_TYPE_ACSUBMIT, AC_Submit },
{ AUTOCONNECT_JSON_TYPE_ACTEXT, AC_Text }
};
ACElement_t t = AC_Unknown;
for (size_t n = 0; n < (sizeof(types) / sizeof(ACElementType_t)); n++) {
if (type.equalsIgnoreCase(String(types[n].tName)))
return types[n].tEnum;
}
return t;
}
/**
* Returns a null element as static storage.
* This static element is referred by invalid JSON data.
* @return A reference of a static element defined by name as null.
*/
AutoConnectElement& AutoConnectAux::_nullElement() {
static AutoConnectElement nullElement("","");
return nullElement;
}
#endif