diff --git a/src/ebusd/mqtthandler.cpp b/src/ebusd/mqtthandler.cpp index 2d158e6f..052aed1e 100755 --- a/src/ebusd/mqtthandler.cpp +++ b/src/ebusd/mqtthandler.cpp @@ -205,7 +205,7 @@ static error_t mqtt_parse_opt(int key, char *arg, struct argp_state *state) { argp_error(state, "duplicate mqtttopic"); return EINVAL; } else { - MqttReplacer replacer; + StringReplacer replacer; if (!replacer.parse(arg, true)) { argp_error(state, "malformed mqtttopic"); return EINVAL; @@ -378,441 +378,6 @@ bool mqtthandler_register(UserInfo* userInfo, BusHandler* busHandler, MessageMap return true; } -/** the known topic field names. */ -static const char* knownFieldNames[] = { - "circuit", - "name", - "field", -}; - -/** the number of known field names. */ -static const size_t knownFieldCount = sizeof(knownFieldNames) / sizeof(char*); - -std::pair MqttReplacer::makeField(const string& name, bool isField) { - if (!isField) { - return {name, -1}; - } - for (int idx = 0; idx < static_cast(knownFieldCount); idx++) { - if (name == knownFieldNames[idx]) { - return {name, idx}; - } - } - return {name, knownFieldCount}; -} - -void MqttReplacer::addPart(ostringstream& stack, int inField) { - string str = stack.str(); - if (inField == 1 && str == "_") { - inField = 0; // single "%_" pattern to reduce to "_" - } else if (inField == 2) { - str = "%{" + str; - inField = 0; - } - if (inField == 0 && str.empty()) { - return; - } - stack.str(""); - if (inField == 0 && !m_parts.empty() && m_parts[m_parts.size()-1].second < 0) { - // append constant to previous constant - m_parts[m_parts.size()-1].first += str; - return; - } - m_parts.push_back(makeField(str, inField > 0)); -} - -bool MqttReplacer::parse(const string& templateStr, bool onlyKnown, bool noKnownDuplicates, bool emptyIfMissing) { - m_parts.clear(); - int inField = 0; // 1 after '%', 2 after '%{' - ostringstream stack; - for (auto ch : templateStr) { - bool empty = stack.tellp() <= 0; - if (ch == '%') { - if (inField == 1 && empty) { // %% for plain % - inField = 0; - stack << ch; - } else { - addPart(stack, inField); - inField = 1; - } - } else if (ch == '{' && inField == 1 && empty) { - inField = 2; - } else if (ch == '}' && inField == 2) { - addPart(stack, 1); - inField = 0; - } else { - if (inField > 0 && !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_')) { - // invalid field character - addPart(stack, inField); - inField = 0; - } - stack << ch; - } - } - addPart(stack, inField); - if (onlyKnown || noKnownDuplicates) { - int foundMask = 0; - int knownCount = knownFieldCount; - for (const auto &it : m_parts) { - if (it.second < 0) { - continue; // unknown field - } - if (onlyKnown && it.second >= knownCount) { - return false; - } - if (noKnownDuplicates && it.second < knownCount) { - int bit = 1 << it.second; - if (foundMask & bit) { - return false; // duplicate known field - } - foundMask |= bit; - } - } - } - m_emptyIfMissing = emptyIfMissing; - return true; -} - -void MqttReplacer::normalize(string& str) { - transform(str.begin(), str.end(), str.begin(), [](unsigned char c){ - return isalnum(c) ? c : '_'; - }); -} - -const string MqttReplacer::str() const { - ostringstream ret; - for (const auto &it : m_parts) { - if (it.second >= 0) { - ret << '%'; - } - ret << it.first; - } - return ret.str(); -} - -void MqttReplacer::ensureDefault() { - if (m_parts.empty()) { - m_parts.emplace_back(string(PACKAGE) + "/", -1); - } else if (m_parts.size() == 1 && m_parts[0].second < 0 && m_parts[0].first.find('/') == string::npos) { - m_parts[0] = {m_parts[0].first + "/", -1}; // ensure trailing slash - } - if (!has("circuit")) { - m_parts.emplace_back("circuit", 0); // index of circuit in knownFieldNames - m_parts.emplace_back("/", -1); - } - if (!has("name")) { - m_parts.emplace_back("name", 1); // index of name in knownFieldNames - } -} - -bool MqttReplacer::empty() const { - return m_parts.empty(); -} - -bool MqttReplacer::has(const string& field) const { - for (const auto &it : m_parts) { - if (it.second >= 0 && it.first == field) { - return true; - } - } - return false; -} - -string MqttReplacer::get(const map& values, bool untilFirstEmpty, bool onlyAlphanum) const { - ostringstream ret; - for (const auto &it : m_parts) { - if (it.second < 0) { - ret << it.first; - continue; - } - const auto pos = values.find(it.first); - if (pos == values.cend()) { - if (untilFirstEmpty) { - break; - } - if (m_emptyIfMissing) { - return ""; - } - } else if (pos->second.empty()) { - if (untilFirstEmpty) { - break; - } - if (m_emptyIfMissing) { - return ""; - } - } else { - ret << pos->second; - } - } - if (!onlyAlphanum) { - return ret.str(); - } - string str = ret.str(); - normalize(str); - return str; -} - -string MqttReplacer::get(const string& circuit, const string& name, const string& fieldName) const { - map values; - values["circuit"] = circuit; - values["name"] = name; - if (!fieldName.empty()) { - values["field"] = fieldName; - } - return get(values, true); -} - -string MqttReplacer::get(const Message* message, const string& fieldName) const { - map values; - values["circuit"] = message->getCircuit(); - values["name"] = message->getName(); - if (!fieldName.empty()) { - values["field"] = fieldName; - } - return get(message->getCircuit(), message->getName(), fieldName); -} - -bool MqttReplacer::isReducable(const map& values) const { - for (const auto &it : m_parts) { - if (it.second < 0) { - continue; - } - const auto pos = values.find(it.first); - if (pos == values.cend()) { - return false; - } - } - return true; -} - -void MqttReplacer::compress(const map& values) { - bool lastConstant = false; - for (auto it = m_parts.begin(); it != m_parts.end(); ) { - bool isConstant = it->second < 0; - if (!isConstant) { - const auto pos = values.find(it->first); - if (pos != values.cend()) { - it->second = -1; - it->first = pos->second; - isConstant = true; - } - } - if (!lastConstant || !isConstant) { - lastConstant = isConstant; - ++it; - continue; - } - (it-1)->first += it->first; - it = m_parts.erase(it); - } -} - -bool MqttReplacer::reduce(const map& values, string& result, bool onlyAlphanum) const { - ostringstream ret; - for (const auto &it : m_parts) { - if (it.second < 0) { - ret << it.first; - continue; - } - const auto pos = values.find(it.first); - if (pos == values.cend()) { - if (m_emptyIfMissing) { - result = ""; - } else { - result = ret.str(); - } - return false; - } - if (m_emptyIfMissing && pos->second.empty()) { - result = ""; - return true; - } - ret << pos->second; - } - result = ret.str(); - if (onlyAlphanum) { - normalize(result); - } - return true; -} - -bool MqttReplacer::checkMatch() const { - bool lastField = false; - for (const auto& part : m_parts) { - bool field = part.second >= 0; - if (field && lastField) { - return false; - } - lastField = field; - } - return true; -} - -ssize_t MqttReplacer::matchTopic(const string& topic, string* circuit, string* name, string* field) const { - size_t last = 0; - size_t count = m_parts.size(); - size_t idx; - bool incomplete = false; - for (idx = 0; idx < count && !incomplete; idx++) { - const auto part = m_parts[idx]; - if (part.second < 0) { - if (topic.substr(last, part.first.length()) != part.first) { - return static_cast(idx); - } - last += part.first.length(); - continue; - } - string value; - if (idx+1 < count) { - size_t pos = topic.find(m_parts[idx+1].first, last); - if (pos == string::npos) { - // next part not found, consume the rest and mark incomplete - value = topic.substr(last); - incomplete = true; - } else { - value = topic.substr(last, pos - last); - } - } else { - // last part is a field name - if (topic.find('/', last) != string::npos) { - // non-name in remainder found - return -static_cast(idx)-1; - } - value = topic.substr(last); - } - last += value.length(); - switch (part.second) { - case 0: *circuit = value; break; - case 1: *name = value; break; - case 2: *field = value; break; - default: // unknown field - break; - } - } - if (incomplete) { - return -static_cast(idx)-1; - } - return static_cast(idx); -} - -static const string EMPTY = ""; - -const string& MqttReplacers::operator[](const string& key) const { - auto itc = m_constants.find(key); - if (itc == m_constants.end()) { - return EMPTY; - } - return itc->second; -} - -bool MqttReplacers::uses(const string& field) const { - for (const auto &it : m_replacers) { - if (it.second.has(field)) { - return true; - } - } - return false; -} - -MqttReplacer& MqttReplacers::get(const string& key) { - MqttReplacer& ret = m_replacers[key]; - auto it = m_constants.find(key); - if (it != m_constants.end()) { - // constant with the same name found - if (ret.empty()) { - ret.parse(it->second); // convert to replacer - } - m_constants.erase(it); - } - return ret; -} - -MqttReplacer MqttReplacers::get(const string& key) const { - const auto& it = m_replacers.find(key); - if (it != m_replacers.cend()) { - return it->second; - } - return MqttReplacer(); -} - -string MqttReplacers::get(const string& key, bool untilFirstEmpty, bool onlyAlphanum, const string& fallbackKey) const { - auto itc = m_constants.find(key); - if (itc != m_constants.end()) { - return itc->second; - } - auto itv = m_replacers.find(key); - if (itv != m_replacers.end()) { - return itv->second.get(m_constants, untilFirstEmpty, onlyAlphanum); - } - if (!fallbackKey.empty()) { - itc = m_constants.find(fallbackKey); - if (itc != m_constants.end()) { - return itc->second; - } - itv = m_replacers.find(fallbackKey); - if (itv != m_replacers.end()) { - return itv->second.get(m_constants, untilFirstEmpty, onlyAlphanum); - } - } - return ""; -} - -bool MqttReplacers::set(const string& key, const string& value, bool removeReplacer) { - m_constants[key] = value; - if (removeReplacer) { - m_replacers.erase(key); - } - if (key.find_first_of("-_") != string::npos) { - return false; - } - string upper = key; - transform(upper.begin(), upper.end(), upper.begin(), ::toupper); - if (upper == key) { - return false; - } - string val = value; - MqttReplacer::normalize(val); - m_constants[upper] = val; - if (removeReplacer) { - m_replacers.erase(upper); - } - return true; -} - -void MqttReplacers::set(const string& key, int value) { - std::ostringstream str; - str << static_cast(value); - m_constants[key] = str.str(); -} - -void MqttReplacers::reduce(bool compress) { - // iterate through variables and reduce as many to constants as possible - bool reduced = false; - do { - reduced = false; - for (auto it = m_replacers.begin(); it != m_replacers.end(); ) { - string str; - if (!it->second.isReducable(m_constants) - || !it->second.reduce(m_constants, str)) { - if (compress) { - it->second.compress(m_constants); - } - ++it; - continue; - } - bool restart = set(it->first, str, false); - it = m_replacers.erase(it); - reduced = true; - if (restart) { - string upper = it->first; - transform(upper.begin(), upper.end(), upper.begin(), ::toupper); - if (m_replacers.erase(upper) > 0) { - break; // restart as iterator is now invalid - } - } - } - } while (reduced); -} - #if (LIBMOSQUITTO_MAJOR >= 1) int on_keypassword(char *buf, int size, int rwflag, void *userdata) { @@ -888,33 +453,6 @@ void on_message( handler->notifyTopic(topic, data); } -void MqttHandler::parseIntegration(const string& line) { - if (line.empty()) { - return; - } - size_t pos = line.find('='); - if (pos == string::npos || pos == 0) { - return; - } - bool emptyIfMissing = false; - string key; - if (line[pos-1] == '?') { - emptyIfMissing = true; - key = line.substr(0, pos-1); - } else { - key = line.substr(0, pos); - } - FileReader::trim(&key); - string value = line.substr(pos+1); - FileReader::trim(&value); - if (value.find('%') == string::npos) { - m_replacers.set(key, value); // constant value - } else { - // simple variable - m_replacers.get(key).parse(value, false, false, emptyIfMissing); - } -} - /** * possible data type names. */ @@ -946,49 +484,21 @@ MqttHandler::MqttHandler(UserInfo* userInfo, BusHandler* busHandler, MessageMap* m_mosquitto = nullptr; bool hasIntegration = false; if (g_integrationFile != nullptr) { - std::ifstream stream; - stream.open(g_integrationFile, std::ifstream::in); - if (!stream.is_open()) { + if (!m_replacers.parseFile(g_integrationFile)) { logOtherError("mqtt", "unable to open integration file %s", g_integrationFile); } else { - string line, last; - while (stream.peek() != EOF && getline(stream, line)) { - if (line.empty()) { - parseIntegration(last); - last = ""; - continue; - } - if (line[0] == '#') { - // only ignore it to allow commented lines in the middle of e.g. payload - continue; - } - if (last.empty()) { - last = line; - } else if (line[0] == '\t' || line[0] == ' ') { // continuation - last += "\n" + line; - } else { - parseIntegration(last); - last = line; - } - } - stream.close(); - parseIntegration(last); hasIntegration = true; if (g_integrationVars) { vector strs; splitFields(g_integrationVars, &strs); for (auto& str : strs) { - size_t pos = str.find('='); - if (pos == string::npos || pos == 0) { - continue; - } - m_replacers.set(str.substr(0, pos), str.substr(pos+1)); + m_replacers.parseLine(str); } } } } // determine topic and prefix - MqttReplacer& topic = m_replacers.get("topic"); + StringReplacer& topic = m_replacers.get("topic"); if (g_topic) { string str = g_topic; bool noDefault = str[str.size()-1] == '#'; @@ -1009,7 +519,7 @@ MqttHandler::MqttHandler(UserInfo* userInfo, BusHandler* busHandler, MessageMap* if (!topic.parse(str, true, true)) { logOtherNotice("mqtt", "unknown or duplicate topic parts potentially prevent matching incoming topics"); topic.parse(str, true); - } else if (!topic.checkMatch()) { + } else if (!topic.checkMatchability()) { logOtherNotice("mqtt", "missing separators between topic parts potentially prevent matching incoming topics"); } } @@ -1229,7 +739,7 @@ void MqttHandler::notifyTopic(const string& topic, const string& data) { logOtherDebug("mqtt", "received topic %s with data %s", topic.c_str(), data.c_str()); string circuit, name, field; - ssize_t match = m_replacers.get("topic").matchTopic(matchTopic, &circuit, &name, &field); + ssize_t match = m_replacers.get("topic").match(matchTopic, &circuit, &name, &field); if (match < 0 && !isList) { logOtherError("mqtt", "received unmatchable topic %s", topic.c_str()); } @@ -1485,7 +995,7 @@ void MqttHandler::run() { continue; } - MqttReplacers msgValues = m_replacers; // need a copy here as the contents are manipulated + StringReplacers msgValues = m_replacers; // need a copy here as the contents are manipulated msgValues.set("circuit", message->getCircuit()); msgValues.set("name", message->getName()); msgValues.set("priority", static_cast(message->getPollPriority())); @@ -1540,7 +1050,7 @@ void MqttHandler::run() { if (str.empty()) { continue; } - MqttReplacers values = msgValues; // need a copy here as the contents are manipulated + StringReplacers values = msgValues; // need a copy here as the contents are manipulated values.set("index", static_cast(index)); values.set("field", fieldName); values.set("fieldname", field->getName(-1)); @@ -1680,7 +1190,7 @@ void MqttHandler::run() { } } -void MqttHandler::publishDefinition(MqttReplacers values, const string& prefix, const string& topic, +void MqttHandler::publishDefinition(StringReplacers values, const string& prefix, const string& topic, const string& circuit, const string& name, const string& fallbackPrefix) { bool reduce = false; if (!topic.empty()) { @@ -1712,7 +1222,7 @@ void MqttHandler::publishDefinition(MqttReplacers values, const string& prefix, publishTopic(defTopic, payload, retain); } -void MqttHandler::publishDefinition(const MqttReplacers& values) { +void MqttHandler::publishDefinition(const StringReplacers& values) { string defTopic = values.get("definition-topic", false); if (defTopic.empty()) { if (needsLog(lf_other, ll_debug)) { diff --git a/src/ebusd/mqtthandler.h b/src/ebusd/mqtthandler.h index a01070e9..e7a4f563 100644 --- a/src/ebusd/mqtthandler.h +++ b/src/ebusd/mqtthandler.h @@ -24,14 +24,14 @@ #include #include #include -#include #include "ebusd/datahandler.h" #include "ebusd/bushandler.h" #include "lib/ebus/message.h" +#include "lib/ebus/stringhelper.h" namespace ebusd { -/** @file ebusd/mqtthandler.h +/** \file ebusd/mqtthandler.h * A data handler enabling MQTT support via mosquitto. */ @@ -56,223 +56,6 @@ const struct argp_child* mqtthandler_getargs(); bool mqtthandler_register(UserInfo* userInfo, BusHandler* busHandler, MessageMap* messages, list* handlers); -/** - * Helper class for replacing a template string with real values. - */ -class MqttReplacer { - public: - /** - * Normalize the string to contain only alpha numeric characters plus underscore by replacing other characters with - * an underscore. - * @param str the string to normalize. - */ - static void normalize(string& str); - - /** - * Get the template string. - * @return the template string (might already be partially reduced). - */ - const string str() const; - - /** - * Parse the template string. - * @param templateStr the template string. - * @param onlyKnown true to allow only known field names from @a knownFieldNames. - * @param noKnownDuplicates true to now allow duplicates from @a knownFieldNames. - * @param emptyIfMissing true when the complete result is supposed to be empty when at least one referenced variable - * is empty or not defined. - * @return true on success, false on malformed template string. - */ - bool parse(const string& templateStr, bool onlyKnown = false, bool noKnownDuplicates = false, - bool emptyIfMissing = false); - - /** - * Ensure the default topic parts are present (circuit and message). - */ - void ensureDefault(); - - /** - * Return whether this replacer is completely empty. - * @return true when empty. - */ - bool empty() const; - - /** - * Return whether the specified field is used. - * @param field the field name to check. - * @return true when the specified field is used. - */ - bool has(const string& field) const; - - /** - * Get the replaced template string. - * @param values the named values for replacement. - * @param untilFirstEmpty true to only return the prefix before the first empty field. - * @param onlyAlphanum whether to only allow alpha numeric characters plus underscore. - * @return the replaced template string. - */ - string get(const map& values, bool untilFirstEmpty = true, bool onlyAlphanum = false) const; - - /** - * Get the replaced template string. - * @param circuit the circuit name for replacement. - * @param name the message name for replacement. - * @param fieldName the field name for replacement. - * @return the replaced template string. - */ - string get(const string& circuit, const string& name, const string& fieldName = "") const; - - /** - * Get the replaced template string. - * @param message the Message from which to extract the values for replacement. - * @param fieldName the field name for replacement. - * @return the replaced template string. - */ - string get(const Message* message, const string& fieldName = "") const; - - /** - * Check if the fields can be reduced to a constant value. - * @param values the named values for replacement. - * @return true if the result is final. - */ - bool isReducable(const map& values) const; - - /** - * Compress all subsequent constant values to a single constant value if possible. - * @param values the named values for replacement. - */ - void compress(const map& values); - - /** - * Reduce the fields to a constant value if possible. - * @param values the named values for replacement. - * @param result the string to store the result in. - * @param onlyAlphanum whether to only allow alpha numeric characters plus underscore. - * @return true if the result is final. - */ - bool reduce(const map& values, string& result, bool onlyAlphanum = false) const; - - /** - * Check match-ability against topics. - * @return true on success, false on bad match-ability. - */ - bool checkMatch() const; - - /** - * Match a topic string against the constant and variables parts. - * @param topic the topic string to match. - * @param circuit pointer to the string receiving the circuit name if present. - * @param name pointer to the string receiving the message name if present. - * @param field pointer to the string receiving the field name if present. - * @return the index of the last unmatched part, or the negative index minus one for extra non-matched non-field parts. - */ - ssize_t matchTopic(const string& topic, string* circuit, string* name, string* field) const; - - private: - /** - * the list of parts the template is composed of. - * the string is either the plain string or the name of the field. - * the number is negative for plain strings, the index to @a knownFieldNames for a known field, or the size of - * @a knownFieldNames for an unknown field. - */ - vector> m_parts; - - /** true when the complete result is supposed to be empty when at least one referenced variable - * is empty or not defined. */ - bool m_emptyIfMissing; - - /** - * Create a named field or constant. - * @param name the plain string or the name of the field. - * @param isField true when it is a field. - * @return the created pair. - */ - static std::pair makeField(const string& name, bool isField); - - /** - * Add a part to the list of parts. - * @param stack the parsing stack. - * @param inField 1 after '%', 2 after '%{', 0 otherwise. - */ - void addPart(ostringstream& stack, int inField); - -}; - - -/** - * A set of constants and @a MqttReplacer variables. - */ -class MqttReplacers { - public: - /** - * Get the value of the specified key from the constants only. - * @param key the key for which to get the value. - * @return the value string or empty. - */ - const string& operator[](const string& key) const; - - /** - * Check if the specified field is used by one of the replacers. - * @param field the name of the field to check. - * @return true if the specified field is used by one of the replacers. - */ - bool uses(const string& field) const; - - /** - * Get the variable value of the specified key. - * @param key the key for which to get the value. - * @return the value @a MqttReplacer. - */ - MqttReplacer& get(const string& key); - - /** - * Get the variable value of the specified key. - * @param key the key for which to get the value. - * @return the value @a MqttReplacer. - */ - MqttReplacer get(const string& key) const; - - /** - * Get the variable or constant value of the specified key. - * @param key the key for which to get the value. - * @param untilFirstEmpty true to only return the prefix before the first empty field. - * @param onlyAlphanum whether to only allow alpha numeric characters plus underscore. - * @param fallbackKey optional fallback key to use when key value is undefined. - * @return the value string or empty. - */ - string get(const string& key, bool untilFirstEmpty, bool onlyAlphanum = false, const string& fallbackKey = "") const; - - /** - * Set the constant value of the specified key and additionally normalized with uppercase key only (if the key does - * not contain an underscore). - * @param key the key to store. - * @param value the value string. - * @param removeReplacer true to remove a replacer with the same name. - * @return true when an upper case key was stored/updates as well. - */ - bool set(const string& key, const string& value, bool removeReplacer = true); - - /** - * Set the constant value of the specified key. - * @param key the key to store. - * @param value the numeric value (converted to a string). - */ - void set(const string& key, int value); - - /** - * Reduce as many variables to constants as possible. - * @param compress true to compress non-reducable replacers if possible. - */ - void reduce(bool compress = false); - - private: - /** constant values from the integration file. */ - map m_constants; - - /** variable values from the integration file. */ - map m_replacers; -}; - /** * The main class supporting MQTT data handling. @@ -287,9 +70,6 @@ class MqttHandler : public DataSink, public DataSource, public WaitThread { */ MqttHandler(UserInfo* userInfo, BusHandler* busHandler, MessageMap* messages); - private: - void parseIntegration(const string& line); - public: /** * Destructor. @@ -333,14 +113,14 @@ class MqttHandler : public DataSink, public DataSource, public WaitThread { * @param name optional name to set before building the topic/payload, or empty. * @param fallbackPrefix optional fallback prefix to use when topic/payload/retain with prefix above is not defined. */ - void publishDefinition(MqttReplacers values, const string& prefix, const string& topic, + void publishDefinition(StringReplacers values, const string& prefix, const string& topic, const string& circuit, const string& name, const string& fallbackPrefix); /** * Publish a definition topic as specified in the given values. * @param values the values with the message specification. */ - void publishDefinition(const MqttReplacers& values); + void publishDefinition(const StringReplacers& values); /** * Called regularly to handle MQTT traffic. @@ -384,7 +164,7 @@ class MqttHandler : public DataSink, public DataSource, public WaitThread { MessageMap* m_messages; /** the global topic replacer. */ - MqttReplacer m_globalTopic; + StringReplacer m_globalTopic; /** the topic to subscribe to. */ string m_subscribeTopic; @@ -395,8 +175,8 @@ class MqttHandler : public DataSink, public DataSource, public WaitThread { /** whether to publish a separate topic for each message field. */ bool m_publishByField; - /** the @a MqttReplacers from the integration file. */ - MqttReplacers m_replacers; + /** the @a StringReplacers from the integration file. */ + StringReplacers m_replacers; /** whether the @a m_replacers includes the definition_topic. */ bool m_hasDefinitionTopic; diff --git a/src/lib/ebus/CMakeLists.txt b/src/lib/ebus/CMakeLists.txt index 6f07f444..5a2f51ea 100644 --- a/src/lib/ebus/CMakeLists.txt +++ b/src/lib/ebus/CMakeLists.txt @@ -1,20 +1,15 @@ add_definitions(-Wconversion -Wno-unused-parameter) set(libebus_a_SOURCES - result.cpp - result.h - symbol.cpp - symbol.h - filereader.h - filereader.cpp - datatype.cpp - datatype.h - data.cpp - data.h - device.cpp - device.h + result.h result.cpp + symbol.h symbol.cpp + filereader.h filereader.cpp + datatype.h datatype.cpp + data.h data.cpp + device.h device.cpp message.cpp message.h + stringhelper.h stringhelper.cpp ) if(HAVE_CONTRIB) diff --git a/src/lib/ebus/Makefile.am b/src/lib/ebus/Makefile.am index fb8efcaa..aa81fd31 100644 --- a/src/lib/ebus/Makefile.am +++ b/src/lib/ebus/Makefile.am @@ -17,7 +17,8 @@ libebus_a_SOURCES = result.cpp \ device.cpp \ device.h \ message.cpp \ - message.h + message.h \ + stringhelper.h stringhelper.cpp if CONTRIB SUBDIRS = contrib diff --git a/src/lib/ebus/stringhelper.cpp b/src/lib/ebus/stringhelper.cpp new file mode 100644 index 00000000..6ca6163e --- /dev/null +++ b/src/lib/ebus/stringhelper.cpp @@ -0,0 +1,524 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2022 John Baier + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include "lib/ebus/stringhelper.h" +#include + +namespace ebusd { + +using std::ostringstream; + +/** the known field names for identifying a message field. */ +static const char* knownFieldNames[] = { + "circuit", + "name", + "field", +}; + +/** the number of known field names. */ +static const size_t knownFieldCount = sizeof(knownFieldNames) / sizeof(char*); + +std::pair StringReplacer::makeField(const string& name, bool isField) { + if (!isField) { + return {name, -1}; + } + for (int idx = 0; idx < static_cast(knownFieldCount); idx++) { + if (name == knownFieldNames[idx]) { + return {name, idx}; + } + } + return {name, knownFieldCount}; +} + +void StringReplacer::addPart(ostringstream& stack, int inField) { + string str = stack.str(); + if (inField == 1 && str == "_") { + inField = 0; // single "%_" pattern to reduce to "_" + } else if (inField == 2) { + str = "%{" + str; + inField = 0; + } + if (inField == 0 && str.empty()) { + return; + } + stack.str(""); + if (inField == 0 && !m_parts.empty() && m_parts[m_parts.size()-1].second < 0) { + // append constant to previous constant + m_parts[m_parts.size()-1].first += str; + return; + } + m_parts.push_back(makeField(str, inField > 0)); +} + +bool StringReplacer::parse(const string& templateStr, bool onlyKnown, bool noKnownDuplicates, bool emptyIfMissing) { + m_parts.clear(); + int inField = 0; // 1 after '%', 2 after '%{' + ostringstream stack; + for (auto ch : templateStr) { + bool empty = stack.tellp() <= 0; + if (ch == '%') { + if (inField == 1 && empty) { // %% for plain % + inField = 0; + stack << ch; + } else { + addPart(stack, inField); + inField = 1; + } + } else if (ch == '{' && inField == 1 && empty) { + inField = 2; + } else if (ch == '}' && inField == 2) { + addPart(stack, 1); + inField = 0; + } else { + if (inField > 0 && !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_')) { + // invalid field character + addPart(stack, inField); + inField = 0; + } + stack << ch; + } + } + addPart(stack, inField); + if (onlyKnown || noKnownDuplicates) { + int foundMask = 0; + int knownCount = knownFieldCount; + for (const auto &it : m_parts) { + if (it.second < 0) { + continue; // unknown field + } + if (onlyKnown && it.second >= knownCount) { + return false; + } + if (noKnownDuplicates && it.second < knownCount) { + int bit = 1 << it.second; + if (foundMask & bit) { + return false; // duplicate known field + } + foundMask |= bit; + } + } + } + m_emptyIfMissing = emptyIfMissing; + return true; +} + +void StringReplacer::normalize(string& str) { + transform(str.begin(), str.end(), str.begin(), [](unsigned char c){ + return isalnum(c) ? c : '_'; + }); +} + +const string StringReplacer::str() const { + ostringstream ret; + for (const auto &it : m_parts) { + if (it.second >= 0) { + ret << '%'; + } + ret << it.first; + } + return ret.str(); +} + +void StringReplacer::ensureDefault(const string& separator) { + if (m_parts.empty()) { + m_parts.emplace_back(string(PACKAGE) + separator, -1); + } else if (m_parts.size() == 1 && m_parts[0].second < 0 && m_parts[0].first.find('/') == string::npos) { + m_parts[0] = {m_parts[0].first + separator, -1}; // ensure trailing slash + } + if (!has("circuit")) { + m_parts.emplace_back("circuit", 0); // index of circuit in knownFieldNames + m_parts.emplace_back(separator, -1); + } + if (!has("name")) { + m_parts.emplace_back("name", 1); // index of name in knownFieldNames + } +} + +bool StringReplacer::empty() const { + return m_parts.empty(); +} + +bool StringReplacer::has(const string& field) const { + for (const auto &it : m_parts) { + if (it.second >= 0 && it.first == field) { + return true; + } + } + return false; +} + +string StringReplacer::get(const map& values, bool untilFirstEmpty, bool onlyAlphanum) const { + ostringstream ret; + for (const auto &it : m_parts) { + if (it.second < 0) { + ret << it.first; + continue; + } + const auto pos = values.find(it.first); + if (pos == values.cend()) { + if (untilFirstEmpty) { + break; + } + if (m_emptyIfMissing) { + return ""; + } + } else if (pos->second.empty()) { + if (untilFirstEmpty) { + break; + } + if (m_emptyIfMissing) { + return ""; + } + } else { + ret << pos->second; + } + } + if (!onlyAlphanum) { + return ret.str(); + } + string str = ret.str(); + normalize(str); + return str; +} + +string StringReplacer::get(const string& circuit, const string& name, const string& fieldName) const { + map values; + values["circuit"] = circuit; + values["name"] = name; + if (!fieldName.empty()) { + values["field"] = fieldName; + } + return get(values, true); +} + +string StringReplacer::get(const Message* message, const string& fieldName) const { + map values; + values["circuit"] = message->getCircuit(); + values["name"] = message->getName(); + if (!fieldName.empty()) { + values["field"] = fieldName; + } + return get(message->getCircuit(), message->getName(), fieldName); +} + +bool StringReplacer::isReducable(const map& values) const { + for (const auto &it : m_parts) { + if (it.second < 0) { + continue; + } + const auto pos = values.find(it.first); + if (pos == values.cend()) { + return false; + } + } + return true; +} + +void StringReplacer::compress(const map& values) { + bool lastConstant = false; + for (auto it = m_parts.begin(); it != m_parts.end(); ) { + bool isConstant = it->second < 0; + if (!isConstant) { + const auto pos = values.find(it->first); + if (pos != values.cend()) { + it->second = -1; + it->first = pos->second; + isConstant = true; + } + } + if (!lastConstant || !isConstant) { + lastConstant = isConstant; + ++it; + continue; + } + (it-1)->first += it->first; + it = m_parts.erase(it); + } +} + +bool StringReplacer::reduce(const map& values, string& result, bool onlyAlphanum) const { + ostringstream ret; + for (const auto &it : m_parts) { + if (it.second < 0) { + ret << it.first; + continue; + } + const auto pos = values.find(it.first); + if (pos == values.cend()) { + if (m_emptyIfMissing) { + result = ""; + } else { + result = ret.str(); + } + return false; + } + if (m_emptyIfMissing && pos->second.empty()) { + result = ""; + return true; + } + ret << pos->second; + } + result = ret.str(); + if (onlyAlphanum) { + normalize(result); + } + return true; +} + +bool StringReplacer::checkMatchability() const { + bool lastField = false; + for (const auto& part : m_parts) { + bool field = part.second >= 0; + if (field && lastField) { + return false; + } + lastField = field; + } + return true; +} + +ssize_t StringReplacer::match(const string& str, string* circuit, string* name, string* field, const string& separator) const { + size_t last = 0; + size_t count = m_parts.size(); + size_t idx; + bool incomplete = false; + for (idx = 0; idx < count && !incomplete; idx++) { + const auto part = m_parts[idx]; + if (part.second < 0) { + if (str.substr(last, part.first.length()) != part.first) { + return static_cast(idx); + } + last += part.first.length(); + continue; + } + string value; + if (idx+1 < count) { + size_t pos = str.find(m_parts[idx+1].first, last); + if (pos == string::npos) { + // next part not found, consume the rest and mark incomplete + value = str.substr(last); + incomplete = true; + } else { + value = str.substr(last, pos - last); + } + } else { + // last part is a field name + if (str.find(separator, last) != string::npos) { + // non-name in remainder found + return -static_cast(idx)-1; + } + value = str.substr(last); + } + last += value.length(); + switch (part.second) { + case 0: *circuit = value; break; + case 1: *name = value; break; + case 2: *field = value; break; + default: // unknown field + break; + } + } + if (incomplete) { + return -static_cast(idx)-1; + } + return static_cast(idx); +} + + +static const string EMPTY = ""; + +const string& StringReplacers::operator[](const string& key) const { + auto itc = m_constants.find(key); + if (itc == m_constants.end()) { + return EMPTY; + } + return itc->second; +} + +void StringReplacers::parseLine(const string& line) { + if (line.empty()) { + return; + } + size_t pos = line.find('='); + if (pos == string::npos || pos == 0) { + return; + } + bool emptyIfMissing = false; + string key; + if (line[pos-1] == '?') { + emptyIfMissing = true; + key = line.substr(0, pos-1); + } else { + key = line.substr(0, pos); + } + FileReader::trim(&key); + string value = line.substr(pos+1); + FileReader::trim(&value); + if (value.find('%') == string::npos) { + set(key, value); // constant value + } else { + // simple variable + get(key).parse(value, false, false, emptyIfMissing); + } +} + +bool StringReplacers::parseFile(const char* filename) { + std::ifstream stream; + stream.open(filename, std::ifstream::in); + if (!stream.is_open()) { + return false; + } + string line, last; + while (stream.peek() != EOF && getline(stream, line)) { + if (line.empty()) { + parseLine(last); + last = ""; + continue; + } + if (line[0] == '#') { + // only ignore it to allow commented lines in the middle of e.g. payload + continue; + } + if (last.empty()) { + last = line; + } else if (line[0] == '\t' || line[0] == ' ') { // continuation + last += "\n" + line; + } else { + parseLine(last); + last = line; + } + } + stream.close(); + parseLine(last); + return true; +} + +bool StringReplacers::uses(const string& field) const { + for (const auto &it : m_replacers) { + if (it.second.has(field)) { + return true; + } + } + return false; +} + +StringReplacer& StringReplacers::get(const string& key) { + StringReplacer& ret = m_replacers[key]; + auto it = m_constants.find(key); + if (it != m_constants.end()) { + // constant with the same name found + if (ret.empty()) { + ret.parse(it->second); // convert to replacer + } + m_constants.erase(it); + } + return ret; +} + +StringReplacer StringReplacers::get(const string& key) const { + const auto& it = m_replacers.find(key); + if (it != m_replacers.cend()) { + return it->second; + } + return StringReplacer(); +} + +string StringReplacers::get(const string& key, bool untilFirstEmpty, bool onlyAlphanum, const string& fallbackKey) const { + auto itc = m_constants.find(key); + if (itc != m_constants.end()) { + return itc->second; + } + auto itv = m_replacers.find(key); + if (itv != m_replacers.end()) { + return itv->second.get(m_constants, untilFirstEmpty, onlyAlphanum); + } + if (!fallbackKey.empty()) { + itc = m_constants.find(fallbackKey); + if (itc != m_constants.end()) { + return itc->second; + } + itv = m_replacers.find(fallbackKey); + if (itv != m_replacers.end()) { + return itv->second.get(m_constants, untilFirstEmpty, onlyAlphanum); + } + } + return ""; +} + +bool StringReplacers::set(const string& key, const string& value, bool removeReplacer) { + m_constants[key] = value; + if (removeReplacer) { + m_replacers.erase(key); + } + if (key.find_first_of("-_") != string::npos) { + return false; + } + string upper = key; + transform(upper.begin(), upper.end(), upper.begin(), ::toupper); + if (upper == key) { + return false; + } + string val = value; + StringReplacer::normalize(val); + m_constants[upper] = val; + if (removeReplacer) { + m_replacers.erase(upper); + } + return true; +} + +void StringReplacers::set(const string& key, int value) { + std::ostringstream str; + str << static_cast(value); + m_constants[key] = str.str(); +} + +void StringReplacers::reduce(bool compress) { + // iterate through variables and reduce as many to constants as possible + bool reduced = false; + do { + reduced = false; + for (auto it = m_replacers.begin(); it != m_replacers.end(); ) { + string str; + if (!it->second.isReducable(m_constants) + || !it->second.reduce(m_constants, str)) { + if (compress) { + it->second.compress(m_constants); + } + ++it; + continue; + } + bool restart = set(it->first, str, false); + it = m_replacers.erase(it); + reduced = true; + if (restart) { + string upper = it->first; + transform(upper.begin(), upper.end(), upper.begin(), ::toupper); + if (m_replacers.erase(upper) > 0) { + break; // restart as iterator is now invalid + } + } + } + } while (reduced); +} + +} // namespace ebusd diff --git a/src/lib/ebus/stringhelper.h b/src/lib/ebus/stringhelper.h new file mode 100644 index 00000000..447c33ef --- /dev/null +++ b/src/lib/ebus/stringhelper.h @@ -0,0 +1,277 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2022 John Baier + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef LIB_EBUS_STRINGHELPER_H_ +#define LIB_EBUS_STRINGHELPER_H_ + +#include +#include +#include +#include +#include +#include +#include "lib/ebus/message.h" + +namespace ebusd { + +/** @file lib/ebus/stringhelper.h + * Helper classes for string replacement. + */ + +using std::map; +using std::ostringstream; +using std::string; +using std::vector; + + +/** + * Helper class for replacing a template string with real values. + */ +class StringReplacer { + public: + /** + * Normalize the string to contain only alpha numeric characters plus underscore by replacing other characters with + * an underscore. + * @param str the string to normalize. + */ + static void normalize(string& str); + + /** + * Get the template string. + * @return the template string (might already be partially reduced). + */ + const string str() const; + + /** + * Parse the template string. + * @param templateStr the template string. + * @param onlyKnown true to allow only known field names from @a knownFieldNames. + * @param noKnownDuplicates true to now allow duplicates from @a knownFieldNames. + * @param emptyIfMissing true when the complete result is supposed to be empty when at least one referenced variable + * is empty or not defined. + * @return true on success, false on malformed template string. + */ + bool parse(const string& templateStr, bool onlyKnown = false, bool noKnownDuplicates = false, + bool emptyIfMissing = false); + + /** + * Ensure the default parts are present (package prefix if empty, circuit and message name). + * @param separator the separator between prefix, circuit, and message name (default slash). + */ + void ensureDefault(const string& separator = "/"); + + /** + * Return whether this replacer is completely empty. + * @return true when empty. + */ + bool empty() const; + + /** + * Return whether the specified field is used. + * @param field the field name to check. + * @return true when the specified field is used. + */ + bool has(const string& field) const; + + /** + * Get the replaced template string. + * @param values the named values for replacement. + * @param untilFirstEmpty true to only return the prefix before the first empty field. + * @param onlyAlphanum whether to only allow alpha numeric characters plus underscore. + * @return the replaced template string. + */ + string get(const map& values, bool untilFirstEmpty = true, bool onlyAlphanum = false) const; + + /** + * Get the replaced template string. + * @param circuit the circuit name for replacement. + * @param name the message name for replacement. + * @param fieldName the field name for replacement. + * @return the replaced template string. + */ + string get(const string& circuit, const string& name, const string& fieldName = "") const; + + /** + * Get the replaced template string. + * @param message the Message from which to extract the values for replacement. + * @param fieldName the field name for replacement. + * @return the replaced template string. + */ + string get(const Message* message, const string& fieldName = "") const; + + /** + * Check if the fields can be reduced to a constant value. + * @param values the named values for replacement. + * @return true if the result is final. + */ + bool isReducable(const map& values) const; + + /** + * Compress all subsequent constant values to a single constant value if possible. + * @param values the named values for replacement. + */ + void compress(const map& values); + + /** + * Reduce the fields to a constant value if possible. + * @param values the named values for replacement. + * @param result the string to store the result in. + * @param onlyAlphanum whether to only allow alpha numeric characters plus underscore. + * @return true if the result is final. + */ + bool reduce(const map& values, string& result, bool onlyAlphanum = false) const; + + /** + * Check match-ability against a string. + * @return true on success, false on bad match-ability. + */ + bool checkMatchability() const; + + /** + * Match a string against the constant and variables parts. + * @param str the string to match. + * @param circuit pointer to the string receiving the circuit name if present. + * @param name pointer to the string receiving the message name if present. + * @param field pointer to the string receiving the field name if present. + * @param separator the separator expected in the extra non-matched non-field parts (default slash). + * @return the index of the last unmatched part, or the negative index minus one for extra non-matched non-field parts. + */ + ssize_t match(const string& str, string* circuit, string* name, string* field, const string& separator = "/") const; + + private: + /** + * the list of parts the template is composed of. + * the string is either the plain string or the name of the field. + * the number is negative for plain strings, the index to @a knownFieldNames for a known field, or the size of + * @a knownFieldNames for an unknown field. + */ + vector> m_parts; + + /** true when the complete result is supposed to be empty when at least one referenced variable + * is empty or not defined. */ + bool m_emptyIfMissing; + + /** + * Create a named field or constant. + * @param name the plain string or the name of the field. + * @param isField true when it is a field. + * @return the created pair. + */ + static std::pair makeField(const string& name, bool isField); + + /** + * Add a part to the list of parts. + * @param stack the parsing stack. + * @param inField 1 after '%', 2 after '%{', 0 otherwise. + */ + void addPart(ostringstream& stack, int inField); + +}; + + +/** + * A set of constants and @a StringReplacer variables. + */ +class StringReplacers { + public: + /** + * Get the value of the specified key from the constants only. + * @param key the key for which to get the value. + * @return the value string or empty. + */ + const string& operator[](const string& key) const; + + /** + * Parse a continuation-normalized line. + * @param line the line to parse. + */ + void parseLine(const string& line); + + /** + * Parse a file with constants and variables. + * @param filename the name (and path) of the file to parse. + * @return true on success, false if the file is not readable. + */ + bool parseFile(const char* filename); + + /** + * Check if the specified field is used by one of the replacers. + * @param field the name of the field to check. + * @return true if the specified field is used by one of the replacers. + */ + bool uses(const string& field) const; + + /** + * Get the variable value of the specified key. + * @param key the key for which to get the value. + * @return the value @a StringReplacer. + */ + StringReplacer& get(const string& key); + + /** + * Get the variable value of the specified key. + * @param key the key for which to get the value. + * @return the value @a StringReplacer. + */ + StringReplacer get(const string& key) const; + + /** + * Get the variable or constant value of the specified key. + * @param key the key for which to get the value. + * @param untilFirstEmpty true to only return the prefix before the first empty field. + * @param onlyAlphanum whether to only allow alpha numeric characters plus underscore. + * @param fallbackKey optional fallback key to use when key value is undefined. + * @return the value string or empty. + */ + string get(const string& key, bool untilFirstEmpty, bool onlyAlphanum = false, const string& fallbackKey = "") const; + + /** + * Set the constant value of the specified key and additionally normalized with uppercase key only (if the key does + * not contain an underscore). + * @param key the key to store. + * @param value the value string. + * @param removeReplacer true to remove a replacer with the same name. + * @return true when an upper case key was stored/updates as well. + */ + bool set(const string& key, const string& value, bool removeReplacer = true); + + /** + * Set the constant value of the specified key. + * @param key the key to store. + * @param value the numeric value (converted to a string). + */ + void set(const string& key, int value); + + /** + * Reduce as many variables to constants as possible. + * @param compress true to compress non-reducable replacers if possible. + */ + void reduce(bool compress = false); + + private: + /** constant values from the integration file. */ + map m_constants; + + /** variable values from the integration file. */ + map m_replacers; +}; + + +} // namespace ebusd + +#endif // LIB_EBUS_STRINGHELPER_H_ diff --git a/src/lib/utils/CMakeLists.txt b/src/lib/utils/CMakeLists.txt index 62e22159..319b5d46 100755 --- a/src/lib/utils/CMakeLists.txt +++ b/src/lib/utils/CMakeLists.txt @@ -8,6 +8,7 @@ set(libutils_a_SOURCES queue.h notify.h rotatefile.h rotatefile.cpp - httpclient.h httpclient.cpp) + httpclient.h httpclient.cpp +) add_library(utils ${libutils_a_SOURCES})