1 /*
2 * Copyright (C) 2005-2018 Team Kodi
3 * This file is part of Kodi - https://kodi.tv
4 *
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 * See LICENSES/README.md for more information.
7 */
8
9 #include "InfoExpression.h"
10
11 #include "GUIInfoManager.h"
12 #include "ServiceBroker.h"
13 #include "guilib/GUIComponent.h"
14 #include "utils/log.h"
15
16 #include <list>
17 #include <memory>
18 #include <stack>
19
20 using namespace INFO;
21
Initialize()22 void InfoSingle::Initialize()
23 {
24 m_condition = CServiceBroker::GetGUI()->GetInfoManager().TranslateSingleString(m_expression, m_listItemDependent);
25 }
26
Update(const CGUIListItem * item)27 void InfoSingle::Update(const CGUIListItem *item)
28 {
29 m_value = CServiceBroker::GetGUI()->GetInfoManager().GetBool(m_condition, m_context, item);
30 }
31
Initialize()32 void InfoExpression::Initialize()
33 {
34 if (!Parse(m_expression))
35 {
36 CLog::Log(LOGERROR, "Error parsing boolean expression %s", m_expression.c_str());
37 m_expression_tree = std::make_shared<InfoLeaf>(CServiceBroker::GetGUI()->GetInfoManager().Register("false", 0), false);
38 }
39 }
40
Update(const CGUIListItem * item)41 void InfoExpression::Update(const CGUIListItem *item)
42 {
43 m_value = m_expression_tree->Evaluate(item);
44 }
45
46 /* Expressions are rewritten at parse time into a form which favours the
47 * formation of groups of associative nodes. These groups are then reordered at
48 * evaluation time such that nodes whose value renders the evaluation of the
49 * remainder of the group unnecessary tend to be evaluated first (these are
50 * true nodes for OR subexpressions, or false nodes for AND subexpressions).
51 * The end effect is to minimise the number of leaf nodes that need to be
52 * evaluated in order to determine the value of the expression. The runtime
53 * adaptability has the advantage of not being customised for any particular skin.
54 *
55 * The modifications to the expression at parse time fall into two groups:
56 * 1) Moving logical NOTs so that they are only applied to leaf nodes.
57 * For example, rewriting ![A+B]|C as !A|!B|C allows reordering such that
58 * any of the three leaves can be evaluated first.
59 * 2) Combining adjacent AND or OR operations such that each path from the root
60 * to a leaf encounters a strictly alternating pattern of AND and OR
61 * operations. So [A|B]|[C|D+[[E|F]|G] becomes A|B|C|[D+[E|F|G]].
62 */
63
Evaluate(const CGUIListItem * item)64 bool InfoExpression::InfoLeaf::Evaluate(const CGUIListItem *item)
65 {
66 return m_invert ^ m_info->Get(item);
67 }
68
InfoAssociativeGroup(node_type_t type,const InfoSubexpressionPtr & left,const InfoSubexpressionPtr & right)69 InfoExpression::InfoAssociativeGroup::InfoAssociativeGroup(
70 node_type_t type,
71 const InfoSubexpressionPtr &left,
72 const InfoSubexpressionPtr &right)
73 : m_type(type)
74 {
75 AddChild(right);
76 AddChild(left);
77 }
78
AddChild(const InfoSubexpressionPtr & child)79 void InfoExpression::InfoAssociativeGroup::AddChild(const InfoSubexpressionPtr &child)
80 {
81 m_children.push_front(child); // largely undoes the effect of parsing right-associative
82 }
83
Merge(const std::shared_ptr<InfoAssociativeGroup> & other)84 void InfoExpression::InfoAssociativeGroup::Merge(const std::shared_ptr<InfoAssociativeGroup>& other)
85 {
86 m_children.splice(m_children.end(), other->m_children);
87 }
88
Evaluate(const CGUIListItem * item)89 bool InfoExpression::InfoAssociativeGroup::Evaluate(const CGUIListItem *item)
90 {
91 /* Handle either AND or OR by using the relation
92 * A AND B == !(!A OR !B)
93 * to convert ANDs into ORs
94 */
95 std::list<InfoSubexpressionPtr>::iterator last = m_children.end();
96 std::list<InfoSubexpressionPtr>::iterator it = m_children.begin();
97 bool use_and = (m_type == NODE_AND);
98 bool result = use_and ^ (*it)->Evaluate(item);
99 while (!result && ++it != last)
100 {
101 result = use_and ^ (*it)->Evaluate(item);
102 if (result)
103 {
104 /* Move this child to the head of the list so we evaluate faster next time */
105 m_children.push_front(*it);
106 m_children.erase(it);
107 }
108 }
109 return use_and ^ result;
110 }
111
112 /* Expressions are parsed using the shunting-yard algorithm. Binary operators
113 * (AND/OR) are treated as right-associative so that we don't need to make a
114 * special case for the unary NOT operator. This has no effect upon the answers
115 * generated, though the initial sequence of evaluation of leaves may be
116 * different from what you might expect.
117 */
118
GetOperator(char ch)119 InfoExpression::operator_t InfoExpression::GetOperator(char ch)
120 {
121 if (ch == '[')
122 return OPERATOR_LB;
123 else if (ch == ']')
124 return OPERATOR_RB;
125 else if (ch == '!')
126 return OPERATOR_NOT;
127 else if (ch == '+')
128 return OPERATOR_AND;
129 else if (ch == '|')
130 return OPERATOR_OR;
131 else
132 return OPERATOR_NONE;
133 }
134
OperatorPop(std::stack<operator_t> & operator_stack,bool & invert,std::stack<InfoSubexpressionPtr> & nodes)135 void InfoExpression::OperatorPop(std::stack<operator_t> &operator_stack, bool &invert, std::stack<InfoSubexpressionPtr> &nodes)
136 {
137 operator_t op2 = operator_stack.top();
138 operator_stack.pop();
139 if (op2 == OPERATOR_NOT)
140 {
141 invert = !invert;
142 }
143 else
144 {
145 // At this point, it can only be OPERATOR_AND or OPERATOR_OR
146 if (invert)
147 op2 = (operator_t) (OPERATOR_AND ^ OPERATOR_OR ^ op2);
148 node_type_t new_type = op2 == OPERATOR_AND ? NODE_AND : NODE_OR;
149
150 InfoSubexpressionPtr right = nodes.top();
151 nodes.pop();
152 InfoSubexpressionPtr left = nodes.top();
153
154 node_type_t right_type = right->Type();
155 node_type_t left_type = left->Type();
156
157 // Combine associative operations into the same node where possible
158 if (left_type == new_type && right_type == new_type)
159 /* For example: AND
160 * / \ ____ AND ____
161 * AND AND -> / / \ \
162 * / \ / \ leaf leaf leaf leaf
163 * leaf leaf leaf leaf
164 */
165 std::static_pointer_cast<InfoAssociativeGroup>(left)->Merge(std::static_pointer_cast<InfoAssociativeGroup>(right));
166 else if (left_type == new_type)
167 /* For example: AND AND
168 * / \ / | \
169 * AND OR -> leaf leaf OR
170 * / \ / \ / \
171 * leaf leaf leaf leaf leaf leaf
172 */
173 std::static_pointer_cast<InfoAssociativeGroup>(left)->AddChild(right);
174 else
175 {
176 nodes.pop();
177 if (right_type == new_type)
178 {
179 /* For example: AND AND
180 * / \ / | \
181 * OR AND -> OR leaf leaf
182 * / \ / \ / \
183 * leaf leaf leaf leaf leaf leaf
184 */
185 std::static_pointer_cast<InfoAssociativeGroup>(right)->AddChild(left);
186 nodes.push(right);
187 }
188 else
189 /* For example: AND which can't be simplified, and
190 * / \ requires a new AND node to be
191 * OR OR created with the two OR nodes
192 * / \ / \ as children
193 * leaf leaf leaf leaf
194 */
195 nodes.push(std::make_shared<InfoAssociativeGroup>(new_type, left, right));
196 }
197 }
198 }
199
Parse(const std::string & expression)200 bool InfoExpression::Parse(const std::string &expression)
201 {
202 const char *s = expression.c_str();
203 std::string operand;
204 std::stack<operator_t> operator_stack;
205 bool invert = false;
206 std::stack<InfoSubexpressionPtr> nodes;
207 // The next two are for syntax-checking purposes
208 bool after_binaryoperator = true;
209 int bracket_count = 0;
210
211 CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();
212
213 char c;
214 // Skip leading whitespace - don't want it to count as an operand if that's all there is
215 while (isspace((unsigned char)(c=*s)))
216 s++;
217
218 while ((c = *s++) != '\0')
219 {
220 operator_t op;
221 if ((op = GetOperator(c)) != OPERATOR_NONE)
222 {
223 // Character is an operator
224 if ((!after_binaryoperator && (c == '!' || c == '[')) ||
225 (after_binaryoperator && (c == ']' || c == '+' || c == '|')))
226 {
227 CLog::Log(LOGERROR, "Misplaced %c", c);
228 return false;
229 }
230 if (c == '[')
231 bracket_count++;
232 else if (c == ']' && bracket_count-- == 0)
233 {
234 CLog::Log(LOGERROR, "Unmatched ]");
235 return false;
236 }
237 if (!operand.empty())
238 {
239 InfoPtr info = infoMgr.Register(operand, m_context);
240 if (!info)
241 {
242 CLog::Log(LOGERROR, "Bad operand '%s'", operand.c_str());
243 return false;
244 }
245 /* Propagate any listItem dependency from the operand to the expression */
246 m_listItemDependent |= info->ListItemDependent();
247 nodes.push(std::make_shared<InfoLeaf>(info, invert));
248 /* Reuse operand string for next operand */
249 operand.clear();
250 }
251
252 // Handle any higher-priority stacked operators, except when the new operator is left-bracket.
253 // For a right-bracket, this will stop with the matching left-bracket at the top of the operator stack.
254 if (op != OPERATOR_LB)
255 {
256 while (!operator_stack.empty() && operator_stack.top() > op)
257 OperatorPop(operator_stack, invert, nodes);
258 }
259 if (op == OPERATOR_RB)
260 operator_stack.pop(); // remove the matching left-bracket
261 else
262 operator_stack.push(op);
263 if (op == OPERATOR_NOT)
264 invert = !invert;
265
266 if (c == '+' || c == '|')
267 after_binaryoperator = true;
268 // Skip trailing whitespace - don't want it to count as an operand if that's all there is
269 while (isspace((unsigned char)(c=*s))) s++;
270 }
271 else
272 {
273 // Character is part of operand
274 operand += c;
275 after_binaryoperator = false;
276 }
277 }
278 if (bracket_count > 0)
279 {
280 CLog::Log(LOGERROR, "Unmatched [");
281 return false;
282 }
283 if (after_binaryoperator)
284 {
285 CLog::Log(LOGERROR, "Missing operand");
286 return false;
287 }
288 if (!operand.empty())
289 {
290 InfoPtr info = infoMgr.Register(operand, m_context);
291 if (!info)
292 {
293 CLog::Log(LOGERROR, "Bad operand '%s'", operand.c_str());
294 return false;
295 }
296 /* Propagate any listItem dependency from the operand to the expression */
297 m_listItemDependent |= info->ListItemDependent();
298 nodes.push(std::make_shared<InfoLeaf>(info, invert));
299 }
300 while (!operator_stack.empty())
301 OperatorPop(operator_stack, invert, nodes);
302
303 m_expression_tree = nodes.top();
304 return true;
305 }
306