/*
* Copyright (C) 2013-2018 Team Kodi
* This file is part of Kodi - https://kodi.tv
*
* SPDX-License-Identifier: GPL-2.0-or-later
* See LICENSES/README.md for more information.
*/
#include "DatabaseQuery.h"
#include "Database.h"
#include "XBDateTime.h"
#include "guilib/LocalizeStrings.h"
#include "utils/CharsetConverter.h"
#include "utils/StringUtils.h"
#include "utils/Variant.h"
#include "utils/XBMCTinyXML.h"
typedef struct
{
char string[15];
CDatabaseQueryRule::SEARCH_OPERATOR op;
int localizedString;
} operatorField;
static const operatorField operators[] = {
{"contains", CDatabaseQueryRule::OPERATOR_CONTAINS, 21400},
{"doesnotcontain", CDatabaseQueryRule::OPERATOR_DOES_NOT_CONTAIN, 21401},
{"is", CDatabaseQueryRule::OPERATOR_EQUALS, 21402},
{"isnot", CDatabaseQueryRule::OPERATOR_DOES_NOT_EQUAL, 21403},
{"startswith", CDatabaseQueryRule::OPERATOR_STARTS_WITH, 21404},
{"endswith", CDatabaseQueryRule::OPERATOR_ENDS_WITH, 21405},
{"greaterthan", CDatabaseQueryRule::OPERATOR_GREATER_THAN, 21406},
{"lessthan", CDatabaseQueryRule::OPERATOR_LESS_THAN, 21407},
{"after", CDatabaseQueryRule::OPERATOR_AFTER, 21408},
{"before", CDatabaseQueryRule::OPERATOR_BEFORE, 21409},
{"inthelast", CDatabaseQueryRule::OPERATOR_IN_THE_LAST, 21410},
{"notinthelast", CDatabaseQueryRule::OPERATOR_NOT_IN_THE_LAST, 21411},
{"true", CDatabaseQueryRule::OPERATOR_TRUE, 20122},
{"false", CDatabaseQueryRule::OPERATOR_FALSE, 20424},
{"between", CDatabaseQueryRule::OPERATOR_BETWEEN, 21456}};
CDatabaseQueryRule::CDatabaseQueryRule()
{
m_field = 0;
m_operator = OPERATOR_CONTAINS;
}
bool CDatabaseQueryRule::Load(const TiXmlNode* node, const std::string& encoding /* = "UTF-8" */)
{
if (node == NULL)
return false;
const TiXmlElement* element = node->ToElement();
if (element == NULL)
return false;
// format is:
// parameter
// where parameter can either be a string or a list of
// tags containing a string
const char* field = element->Attribute("field");
const char* oper = element->Attribute("operator");
if (field == NULL || oper == NULL)
return false;
m_field = TranslateField(field);
m_operator = TranslateOperator(oper);
if (m_operator == OPERATOR_TRUE || m_operator == OPERATOR_FALSE)
return true;
const TiXmlNode* parameter = element->FirstChild();
if (parameter == NULL)
return false;
if (parameter->Type() == TiXmlNode::TINYXML_TEXT)
{
std::string utf8Parameter;
if (encoding.empty()) // utf8
utf8Parameter = parameter->ValueStr();
else
g_charsetConverter.ToUtf8(encoding, parameter->ValueStr(), utf8Parameter);
if (!utf8Parameter.empty())
m_parameter.push_back(utf8Parameter);
}
else if (parameter->Type() == TiXmlNode::TINYXML_ELEMENT)
{
const TiXmlNode* valueNode = element->FirstChild("value");
while (valueNode != NULL)
{
const TiXmlNode* value = valueNode->FirstChild();
if (value != NULL && value->Type() == TiXmlNode::TINYXML_TEXT)
{
std::string utf8Parameter;
if (encoding.empty()) // utf8
utf8Parameter = value->ValueStr();
else
g_charsetConverter.ToUtf8(encoding, value->ValueStr(), utf8Parameter);
if (!utf8Parameter.empty())
m_parameter.push_back(utf8Parameter);
}
valueNode = valueNode->NextSibling("value");
}
}
else
return false;
return true;
}
bool CDatabaseQueryRule::Load(const CVariant& obj)
{
if (!obj.isMember("field") || !obj["field"].isString() || !obj.isMember("operator") ||
!obj["operator"].isString())
return false;
m_field = TranslateField(obj["field"].asString().c_str());
m_operator = TranslateOperator(obj["operator"].asString().c_str());
if (m_operator == OPERATOR_TRUE || m_operator == OPERATOR_FALSE)
return true;
if (!obj.isMember("value") || (!obj["value"].isString() && !obj["value"].isArray()))
return false;
const CVariant& value = obj["value"];
if (value.isString())
m_parameter.push_back(value.asString());
else if (value.isArray())
{
for (CVariant::const_iterator_array val = value.begin_array(); val != value.end_array(); ++val)
{
if (val->isString() && !val->asString().empty())
m_parameter.push_back(val->asString());
}
if (m_parameter.empty())
m_parameter.emplace_back("");
}
else
return false;
return true;
}
bool CDatabaseQueryRule::Save(TiXmlNode* parent) const
{
if (parent == NULL ||
(m_parameter.empty() && m_operator != OPERATOR_TRUE && m_operator != OPERATOR_FALSE))
return false;
TiXmlElement rule("rule");
rule.SetAttribute("field", TranslateField(m_field).c_str());
rule.SetAttribute("operator", TranslateOperator(m_operator).c_str());
for (const auto& it : m_parameter)
{
TiXmlElement value("value");
TiXmlText text(it);
value.InsertEndChild(text);
rule.InsertEndChild(value);
}
parent->InsertEndChild(rule);
return true;
}
bool CDatabaseQueryRule::Save(CVariant& obj) const
{
if (obj.isNull() ||
(m_parameter.empty() && m_operator != OPERATOR_TRUE && m_operator != OPERATOR_FALSE))
return false;
obj["field"] = TranslateField(m_field);
obj["operator"] = TranslateOperator(m_operator);
obj["value"] = m_parameter;
return true;
}
CDatabaseQueryRule::SEARCH_OPERATOR CDatabaseQueryRule::TranslateOperator(const char* oper)
{
for (const operatorField& o : operators)
if (StringUtils::EqualsNoCase(oper, o.string))
return o.op;
return OPERATOR_CONTAINS;
}
std::string CDatabaseQueryRule::TranslateOperator(SEARCH_OPERATOR oper)
{
for (const operatorField& o : operators)
if (oper == o.op)
return o.string;
return "contains";
}
std::string CDatabaseQueryRule::GetLocalizedOperator(SEARCH_OPERATOR oper)
{
for (const operatorField& o : operators)
if (oper == o.op)
return g_localizeStrings.Get(o.localizedString);
return g_localizeStrings.Get(16018);
}
void CDatabaseQueryRule::GetAvailableOperators(std::vector& operatorList)
{
for (const operatorField& o : operators)
operatorList.emplace_back(o.string);
}
std::string CDatabaseQueryRule::GetParameter() const
{
return StringUtils::Join(m_parameter, DATABASEQUERY_RULE_VALUE_SEPARATOR);
}
void CDatabaseQueryRule::SetParameter(const std::string& value)
{
m_parameter = StringUtils::Split(value, DATABASEQUERY_RULE_VALUE_SEPARATOR);
}
void CDatabaseQueryRule::SetParameter(const std::vector& values)
{
m_parameter.assign(values.begin(), values.end());
}
std::string CDatabaseQueryRule::ValidateParameter(const std::string& parameter) const
{
if ((GetFieldType(m_field) == REAL_FIELD || GetFieldType(m_field) == NUMERIC_FIELD ||
GetFieldType(m_field) == SECONDS_FIELD) &&
parameter.empty())
return "0"; // interpret empty fields as 0
return parameter;
}
std::string CDatabaseQueryRule::FormatParameter(const std::string& operatorString,
const std::string& param,
const CDatabase& db,
const std::string& strType) const
{
std::string parameter;
if (GetFieldType(m_field) == TEXTIN_FIELD)
{
std::vector split = StringUtils::Split(param, ',');
for (std::string& itIn : split)
{
if (!parameter.empty())
parameter += ",";
parameter += db.PrepareSQL("'%s'", StringUtils::Trim(itIn).c_str());
}
parameter = " IN (" + parameter + ")";
}
else
parameter = db.PrepareSQL(operatorString, ValidateParameter(param).c_str());
if (GetFieldType(m_field) == DATE_FIELD)
{
if (m_operator == OPERATOR_IN_THE_LAST || m_operator == OPERATOR_NOT_IN_THE_LAST)
{ // translate time period
CDateTime date = CDateTime::GetCurrentDateTime();
CDateTimeSpan span;
span.SetFromPeriod(param);
date -= span;
parameter = db.PrepareSQL(operatorString, date.GetAsDBDate().c_str());
}
}
return parameter;
}
std::string CDatabaseQueryRule::GetOperatorString(SEARCH_OPERATOR op) const
{
std::string operatorString;
if (GetFieldType(m_field) != TEXTIN_FIELD)
{
// the comparison piece
switch (op)
{
case OPERATOR_CONTAINS:
operatorString = " LIKE '%%%s%%'";
break;
case OPERATOR_DOES_NOT_CONTAIN:
operatorString = " LIKE '%%%s%%'";
break;
case OPERATOR_EQUALS:
if (GetFieldType(m_field) == REAL_FIELD || GetFieldType(m_field) == NUMERIC_FIELD ||
GetFieldType(m_field) == SECONDS_FIELD)
operatorString = " = %s";
else
operatorString = " LIKE '%s'";
break;
case OPERATOR_DOES_NOT_EQUAL:
if (GetFieldType(m_field) == REAL_FIELD || GetFieldType(m_field) == NUMERIC_FIELD ||
GetFieldType(m_field) == SECONDS_FIELD)
operatorString = " != %s";
else
operatorString = " LIKE '%s'";
break;
case OPERATOR_STARTS_WITH:
operatorString = " LIKE '%s%%'";
break;
case OPERATOR_ENDS_WITH:
operatorString = " LIKE '%%%s'";
break;
case OPERATOR_AFTER:
case OPERATOR_GREATER_THAN:
case OPERATOR_IN_THE_LAST:
operatorString = " > ";
if (GetFieldType(m_field) == REAL_FIELD || GetFieldType(m_field) == NUMERIC_FIELD ||
GetFieldType(m_field) == SECONDS_FIELD)
operatorString += "%s";
else
operatorString += "'%s'";
break;
case OPERATOR_BEFORE:
case OPERATOR_LESS_THAN:
case OPERATOR_NOT_IN_THE_LAST:
operatorString = " < ";
if (GetFieldType(m_field) == REAL_FIELD || GetFieldType(m_field) == NUMERIC_FIELD ||
GetFieldType(m_field) == SECONDS_FIELD)
operatorString += "%s";
else
operatorString += "'%s'";
break;
case OPERATOR_TRUE:
operatorString = " = 1";
break;
case OPERATOR_FALSE:
operatorString = " = 0";
break;
default:
break;
}
}
return operatorString;
}
std::string CDatabaseQueryRule::GetWhereClause(const CDatabase& db,
const std::string& strType) const
{
SEARCH_OPERATOR op = GetOperator(strType);
std::string operatorString = GetOperatorString(op);
std::string negate;
if (op == OPERATOR_DOES_NOT_CONTAIN || op == OPERATOR_FALSE ||
(op == OPERATOR_DOES_NOT_EQUAL && GetFieldType(m_field) != REAL_FIELD &&
GetFieldType(m_field) != NUMERIC_FIELD && GetFieldType(m_field) != SECONDS_FIELD))
negate = " NOT ";
// boolean operators don't have any values in m_parameter, they work on the operator
if (m_operator == OPERATOR_FALSE || m_operator == OPERATOR_TRUE)
return GetBooleanQuery(negate, strType);
// Process boolean field with (not) EQUAL/CONTAINS "true"/"false" parameter too
if (GetFieldType(m_field) == BOOLEAN_FIELD &&
(m_parameter[0] == "true" || m_parameter[0] == "false") &&
(op == OPERATOR_CONTAINS || op == OPERATOR_EQUALS || op == OPERATOR_DOES_NOT_CONTAIN ||
op == OPERATOR_DOES_NOT_EQUAL))
{
if (m_parameter[0] == "false")
{
if (!negate.empty())
negate.clear();
else
negate = " NOT ";
}
return GetBooleanQuery(negate, strType);
}
// The BETWEEN operator is handled special
if (op == OPERATOR_BETWEEN)
{
if (m_parameter.size() != 2)
return "";
FIELD_TYPE fieldType = GetFieldType(m_field);
if (fieldType == REAL_FIELD)
return db.PrepareSQL("%s BETWEEN %s AND %s", GetField(m_field, strType).c_str(),
m_parameter[0].c_str(), m_parameter[1].c_str());
else if (fieldType == NUMERIC_FIELD)
return db.PrepareSQL("CAST(%s as DECIMAL(5,1)) BETWEEN %s AND %s",
GetField(m_field, strType).c_str(), m_parameter[0].c_str(),
m_parameter[1].c_str());
else if (fieldType == SECONDS_FIELD)
return db.PrepareSQL("CAST(%s as INTEGER) BETWEEN %s AND %s",
GetField(m_field, strType).c_str(), m_parameter[0].c_str(),
m_parameter[1].c_str());
else
return db.PrepareSQL("%s BETWEEN '%s' AND '%s'", GetField(m_field, strType).c_str(),
m_parameter[0].c_str(), m_parameter[1].c_str());
}
// now the query parameter
std::string wholeQuery;
for (std::vector::const_iterator it = m_parameter.begin(); it != m_parameter.end();
++it)
{
std::string query = '(' + FormatWhereClause(negate, operatorString, *it, db, strType) + ')';
if (it + 1 != m_parameter.end())
{
if (negate.empty())
query += " OR ";
else
query += " AND ";
}
wholeQuery += query;
}
return wholeQuery;
}
std::string CDatabaseQueryRule::FormatWhereClause(const std::string& negate,
const std::string& oper,
const std::string& param,
const CDatabase& db,
const std::string& strType) const
{
std::string parameter = FormatParameter(oper, param, db, strType);
std::string query;
if (m_field != 0)
{
std::string fmt = "{}";
if (GetFieldType(m_field) == NUMERIC_FIELD)
fmt = "CAST({} as DECIMAL(6,1))";
else if (GetFieldType(m_field) == SECONDS_FIELD)
fmt = "CAST({} as INTEGER)";
query = StringUtils::Format(fmt, GetField(m_field, strType));
query += negate + parameter;
// special case for matching parameters in fields that might be either empty or NULL.
if ((param.empty() && negate.empty()) || (!param.empty() && !negate.empty()))
query += " OR " + GetField(m_field, strType) + " IS NULL";
}
if (query == negate + parameter)
query = "1";
return query;
}
void CDatabaseQueryRuleCombination::clear()
{
m_combinations.clear();
m_rules.clear();
m_type = CombinationAnd;
}
std::string CDatabaseQueryRuleCombination::GetWhereClause(const CDatabase& db,
const std::string& strType) const
{
std::string rule;
// translate the combinations into SQL
for (CDatabaseQueryRuleCombinations::const_iterator it = m_combinations.begin();
it != m_combinations.end(); ++it)
{
if (it != m_combinations.begin())
rule += m_type == CombinationAnd ? " AND " : " OR ";
rule += "(" + (*it)->GetWhereClause(db, strType) + ")";
}
// translate the rules into SQL
for (const auto& it : m_rules)
{
if (!rule.empty())
rule += m_type == CombinationAnd ? " AND " : " OR ";
rule += "(";
std::string currentRule = it->GetWhereClause(db, strType);
// if we don't get a rule, we add '1' or '0' so the query is still valid and doesn't fail
if (currentRule.empty())
currentRule = m_type == CombinationAnd ? "'1'" : "'0'";
rule += currentRule;
rule += ")";
}
return rule;
}
bool CDatabaseQueryRuleCombination::Load(const CVariant& obj,
const IDatabaseQueryRuleFactory* factory)
{
if (!obj.isObject() && !obj.isArray())
return false;
CVariant child;
if (obj.isObject())
{
if (obj.isMember("and") && obj["and"].isArray())
{
m_type = CombinationAnd;
child = obj["and"];
}
else if (obj.isMember("or") && obj["or"].isArray())
{
m_type = CombinationOr;
child = obj["or"];
}
else
return false;
}
else
child = obj;
for (CVariant::const_iterator_array it = child.begin_array(); it != child.end_array(); ++it)
{
if (!it->isObject())
continue;
if (it->isMember("and") || it->isMember("or"))
{
std::shared_ptr combo(factory->CreateCombination());
if (combo && combo->Load(*it, factory))
m_combinations.push_back(combo);
}
else
{
std::shared_ptr rule(factory->CreateRule());
if (rule && rule->Load(*it))
m_rules.push_back(rule);
}
}
return true;
}
bool CDatabaseQueryRuleCombination::Save(TiXmlNode* parent) const
{
for (const auto& it : m_rules)
it->Save(parent);
return true;
}
bool CDatabaseQueryRuleCombination::Save(CVariant& obj) const
{
if (!obj.isObject() || (m_combinations.empty() && m_rules.empty()))
return false;
CVariant comboArray(CVariant::VariantTypeArray);
if (!m_combinations.empty())
{
for (const auto& combo : m_combinations)
{
CVariant comboObj(CVariant::VariantTypeObject);
if (combo->Save(comboObj))
comboArray.push_back(comboObj);
}
}
if (!m_rules.empty())
{
for (const auto& rule : m_rules)
{
CVariant ruleObj(CVariant::VariantTypeObject);
if (rule->Save(ruleObj))
comboArray.push_back(ruleObj);
}
}
obj[TranslateCombinationType()] = comboArray;
return true;
}
std::string CDatabaseQueryRuleCombination::TranslateCombinationType() const
{
return m_type == CombinationAnd ? "and" : "or";
}