Fix filter behaviour, fix statusdat filter

refs #4469
This commit is contained in:
Jannis Moßhammer 2013-10-21 17:04:05 +02:00
parent 8cf1e8cbf5
commit c4f3e78c02
14 changed files with 298 additions and 79 deletions

View File

@ -72,7 +72,6 @@ class FilterController extends ActionController
$this->getParam('filter_module')
);
$urlTarget = $this->parse($query, $target);
die(print_r($urlTarget,true));
$this->redirect($urlTarget['urlParam']);
}
@ -108,7 +107,8 @@ class FilterController extends ActionController
return array(
'state' => 'success',
'proposals' => $this->registry->getProposalsForQuery($text),
'urlParam' => $registry::getUrlForTarget($target, $queryTree)
'urlParam' => $registry::getUrlForTarget($target, $queryTree),
'valid' => count($this->registry->getIgnoredQueryParts()) === 0
);
} catch (\Exception $exc) {
Logger::error($exc);

View File

@ -66,13 +66,22 @@ class TreeToSqlParser
* @param String $operator The operator from the query node
* @return string The operator for the sql query part
*/
private function getSqlOperator($operator)
private function getSqlOperator($operator, array $right)
{
switch($operator) {
case Node::OPERATOR_EQUALS:
return 'LIKE';
if (count($right) > 1) {
return 'IN';
} else {
return 'LIKE';
}
case Node::OPERATOR_EQUALS_NOT:
return 'NOT LIKE';
if (count($right) > 1) {
return 'NOT IN';
} else {
return 'NOT LIKE';
}
default:
return $operator;
}
@ -125,14 +134,14 @@ class TreeToSqlParser
return '';
}
$this->query->requireColumn($node->left);
$queryString = $this->query->getMappedField($node->left);
$queryString = '(' . $this->query->getMappedField($node->left) . ')';
if ($this->query->isAggregateColumn($node->left)) {
$this->type = 'HAVING';
}
$queryString .= ' ' . (is_integer($node->right) ?
$node->operator : $this->getSqlOperator($node->operator)) . ' ';
$queryString .= $this->getParameterValue($node);
$node->operator : $this->getSqlOperator($node->operator, $node->right)) . ' ';
$queryString = $this->addValueToQuery($node, $queryString);
return $queryString;
}
@ -145,18 +154,27 @@ class TreeToSqlParser
* @param Node $node The node to retrieve the sql string value from
* @return String|int The converted and quoted value
*/
private function getParameterValue(Node $node) {
$value = $node->right;
if ($node->operator === Node::OPERATOR_EQUALS || $node->operator === Node::OPERATOR_EQUALS_NOT) {
$value = str_replace('*', '%', $value);
private function addValueToQuery(Node $node, $query) {
$values = array();
foreach ($node->right as $value) {
if ($node->operator === Node::OPERATOR_EQUALS || $node->operator === Node::OPERATOR_EQUALS_NOT) {
$value = str_replace('*', '%', $value);
}
if ($this->query->isTimestamp($node->left)) {
$node->context = Node::CONTEXT_TIMESTRING;
}
if ($node->context === Node::CONTEXT_TIMESTRING) {
$value = strtotime($value);
}
$values[] = $this->query->getDatasource()->getConnection()->quote($value);
}
if ($this->query->isTimestamp($node->left)) {
$node->context = Node::CONTEXT_TIMESTRING;
$valueString = join(',', $values);
if (count($values) > 1) {
return '( '. $valueString . ')';
}
if ($node->context === Node::CONTEXT_TIMESTRING) {
$value = strtotime($value);
}
return $this->query->getDatasource()->getConnection()->quote($value);
return $query . $valueString;
}
/**

View File

@ -186,7 +186,7 @@ class Filter extends QueryProposer
}
$proposals = array_merge($proposals, $this->getDefaultDomain()->getProposalsForQuery($query));
}
return $proposals;
return array_unique($proposals);
}
/**
@ -197,7 +197,7 @@ class Filter extends QueryProposer
*/
private function splitQueryAtNextConjunction($query)
{
$delimiter = array('AND', 'OR');
$delimiter = array('AND'/*, 'OR'*/); // or is not supported currently
$inStr = false;
for ($i = 0; $i < strlen($query); $i++) {
// Skip strings

View File

@ -113,6 +113,14 @@ class Node
$node->type = self::TYPE_OPERATOR;
$node->operator = $operator;
$node->left = $left;
if ($right === null) {
$right = array();
} elseif (!is_array($right)) {
$right = array($right);
}
foreach($right as &$value) {
$value = trim($value);
}
$node->right = $right;
return $node;
}

View File

@ -54,6 +54,23 @@ class Tree
*/
private $lastNode;
public function insertTree(Tree $tree)
{
$this->insertSubTree($tree->root);
$this->root = $this->normalizeTree($this->root);
}
private function insertSubTree(Node $node)
{
if ($node->type === Node::TYPE_OPERATOR) {
$this->insert($node);
} else {
$this->insert($node->type === Node::TYPE_AND ? Node::createAndNode() : Node::createOrNode());
$this->insertSubTree($node->left);
$this->insertSubTree($node->right);
}
}
/**
* Insert a node into this tree, recognizing type and insert position
*
@ -77,7 +94,6 @@ class Tree
$this->insert(Node::createAndNode());
}
$node->parent = $this->lastNode;
if ($this->lastNode->left == null) {
$this->lastNode->left = $node;
} elseif ($this->lastNode->right == null) {
@ -86,6 +102,7 @@ class Tree
break;
}
}
$this->lastNode = $node;
}
@ -350,16 +367,23 @@ class Tree
if ($ctx === null) {
return null;
}
if ($ctx->type === Node::TYPE_OPERATOR) {
if ($ctx->left == $node->left && $ctx->right == $node->right && $ctx->operator == $node->operator) {
return $ctx;
if ($ctx->type == Node::TYPE_OPERATOR) {
if ($ctx->left == $node->left && $ctx->operator == $node->operator) {
if(empty($node->right) || $ctx->right == $node->right) {
return $ctx;
}
}
return null;
} else {
$result = $this->findNode($node, $ctx->left);
if ($result === null) {
$result = null;
if ($ctx->left) {
$result = $this->findNode($node, $ctx->left);
} if ($result == null && $ctx->right) {
$result = $this->findNode($node, $ctx->right);
}
return $result;
}
}
@ -367,21 +391,25 @@ class Tree
/**
* Return true if A node with the given attribute on the left side exists
*
* @param String $name The attribute to test for existence
* @param Node $ctx The current root node
* @param String $name The attribute to test for existence
* @param Node $ctx The current root node
* @oaram bool $isRecursive Internal flag to disable null nodes being replaced with the tree root
*
* @return bool True if a node contains $name on the left side, otherwise false
*/
public function hasNodeWithAttribute($name, $ctx = null)
public function hasNodeWithAttribute($name, $ctx = null, $isRecursive = false)
{
$ctx = $ctx ? $ctx : $this->root;
if (!$isRecursive) {
$ctx = $ctx ? $ctx : $this->root;
}
if ($ctx === null) {
return false;
}
if ($ctx->type === Node::TYPE_OPERATOR) {
return $ctx->left === $name;
} else {
return $this->hasNodeWithAttribute($name, $ctx->left) || $this->hasNodeWithAttribute($name, $ctx->right);
return $this->hasNodeWithAttribute($name, $ctx->left, true)
|| $this->hasNodeWithAttribute($name, $ctx->right, true);
}
}
}

View File

@ -225,7 +225,8 @@ class Query extends BaseQuery
$indexes = array_keys($state[$target]);
if ($baseGroup) {
$baseGroup->setQuery($this);
$indexes = $baseGroup->filter($state[$target]);
$idx = array_keys($state[$target]);
$indexes = $baseGroup->filter($state[$target], $idx );
}
if (!isset($result[$target])) {
$result[$target] = $indexes;

View File

@ -28,6 +28,8 @@
namespace Icinga\Protocol\Statusdat\Query;
use Icinga\Protocol\Ldap\Exception;
class Expression implements IQueryPart
{
/**
@ -117,12 +119,18 @@ class Expression implements IQueryPart
case "LIKE":
$this->CB = "isLike";
break;
case "NOT_LIKE":
$this->CB = "isNotLike";
break;
case "!=":
$this->CB = "isNotEqual";
break;
case "IN":
$this->CB = "isIn";
break;
case "NOT_IN":
$this->CB = "isNotIn";
break;
default:
throw new \Exception("Unknown operator $token in expression $this->expression !");
}
@ -216,7 +224,6 @@ class Expression implements IQueryPart
$idx = array_keys($base);
}
$this->basedata = $base;
return array_filter($idx, array($this, "filterFn"));
}
@ -248,12 +255,24 @@ class Expression implements IQueryPart
return false;
}
if ($this->CB == "isIn") {
return count(array_intersect($values, $this->value)) > 0;
}
if ($this->CB == "isNotIn") {
return count(array_intersect($values, $this->value)) == 0;
if ($this->CB == "isIn" || $this->CB == "isNotIn") {
$cmpValues = is_array($this->value) ? $this->value : array($this->value);
foreach ($cmpValues as $cmpValue) {
$this->value = $cmpValue;
foreach ($values as $value) {
if ($this->CB == "isIn" && $this->isLike($value)) {
$this->value = $cmpValues;
return true;
} elseif ($this->CB == "isNotIn" && $this->isNotLike($value)) {
$this->value = $cmpValues;
return true;
}
}
}
$this->value = $cmpValues;
return false;
}
if ($this->function) {
$values = call_user_func($this->function, $values);
if (!is_array($values)) {
@ -355,6 +374,15 @@ class Expression implements IQueryPart
return preg_match("/^" . str_replace("%", ".*", $this->value) . "$/", $value) ? true : false;
}
/**
* @param $value
* @return bool
*/
public function isNotLike($value)
{
return !preg_match("/^" . str_replace("%", ".*", $this->value) . "$/", $value) ? true : false;
}
/**
* @param $value
* @return bool

View File

@ -26,30 +26,48 @@
*/
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Protocol\Statusdat;
use Icinga\Filter\Filterable;
use Icinga\Filter\Query\Node;
use Icinga\Filter\Query\Tree;
use Icinga\Protocol\Statusdat\Query\Expression;
use Icinga\Protocol\Statusdat\Query\Group;
use Icinga\Protocol\Statusdat\Query\IQueryPart;
/**
* Parser to create statusdat filter expressions from query trees
*
*/
class TreeToStatusdatQueryParser
{
private function nodeToQuery(Node $node, Filterable $source)
/**
* Create a Statusdat expression from a Tree node
*
* @param Node $node The node to convert to an expression
* @param Filterable $source The filterable to use for field mapping
*
* @return IQueryPart Either a statusdat expression or an expression group
*/
private function nodeToQuery(Node $node, Filterable $source)
{
if ($node->type === Node::TYPE_OPERATOR) {
$op = $node->operator;
$value = $node->right;
$node->left = $source->getMappedField($node->left);
if (stripos($node->right, '*') !== false) {
$op = 'LIKE';
$op = 'IN';
$values = $node->right;
if ($node->operator === NODE::OPERATOR_EQUALS_NOT) {
$op = 'NOT_' . $op;
}
foreach ($values as &$value) {
$value = str_replace('*', '%', $value);
}
return new Expression($node->left . ' ' . $op . ' ?', $value);
$values = array($values);
return new Expression($node->left . ' ' . $op . ' ? ', $values);
} else {
$group = new Group();
$group->setType(($node->type === Node::TYPE_OR) ? Group::TYPE_OR : Group::TYPE_AND);
@ -60,6 +78,14 @@ class TreeToStatusdatQueryParser
}
/**
* Create a statusdat specific filter expression for the given query tree and filterable
*
* @param Tree $tree The tree to convert to a query
* @param Filterable $source The filterable to use for tree conversion
*
* @return IQueryPart A statusdat query object
*/
public function treeToQuery(Tree $tree, Filterable $source)
{
@ -70,4 +96,4 @@ class TreeToStatusdatQueryParser
}
return null;
}
}
}

View File

@ -48,6 +48,29 @@ class FilterBadgeRenderer implements Widget
private $conjunctionCellar = '';
private $urlFilter;
private $tpl =<<<'EOT'
<div class="btn-group">
<a title="Click To Remove" class="btn btn-default btn-xs dropdown-toggle" href="{{REMOVE_FILTER}}" data-icinga-target="self">
{{FILTER_SUM}}
</a>
{{SUBLIST}}
</div>
EOT;
private $subTpl =<<<'EOT'
<button type="button" class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
{{SUBFILTER_LIST}}
</ul>
EOT;
private $subItemTpl =<<<'EOT'
<li><a title="Click To Remove" href="{{REMOVE_FILTER}}" data-icinga-target="self">{{FILTER_TEXT}}</a></li>
EOT;
/**
* Create a new badge renderer for this tree
*
@ -58,6 +81,41 @@ class FilterBadgeRenderer implements Widget
$this->tree = $tree;
}
private function getSummarizedText($node)
{
if (count($node->right) === 1) {
$value = $node->right[0];
} else {
$value = join(',', $node->right);
if(strlen($value) > 15) {
$value = substr($value, 0, 13) . '..';
}
}
return $this->conjunctionCellar . ' '. ucfirst($node->left) . $node->operator . $value ;
}
private function getSubEntries(Node $node)
{
$liItems = "";
$basePath = $this->baseUrl->getAbsoluteUrl();
$allParams = $this->baseUrl->getParams();
foreach ($node->right as $value) {
$newTree = $this->tree->createCopy();
$affectedNode = $newTree->findNode($node);
$affectedNode->right = array_diff($affectedNode->right, array($value));
$url = $this->urlFilter->fromTree($newTree);
$url = $basePath . (empty($allParams) ? '?' : '&') . $url;
$liItem = str_replace('{{REMOVE_FILTER}}', $url, $this->subItemTpl);
$liItem = str_replace('{{FILTER_TEXT}}', ucfirst($node->left) . $node->operator . $value , $liItem);
$liItems .= $liItem;
}
return str_replace('{{SUBFILTER_LIST}}', $liItems, $this->subTpl);
}
/**
* Create a removable badge from a query tree node
*
@ -74,15 +132,18 @@ class FilterBadgeRenderer implements Widget
$newTree = $this->tree->withoutNode($node);
$url = $this->urlFilter->fromTree($newTree);
$url = $basePath . (empty($allParams) ? '?' : '&') . $url;
$sumText = $this->getSummarizedText($node);
return ' <a class="filter-badge btn btn-default btn-xs" href="' . $url . '">'
. $this->conjunctionCellar . ' '
. ucfirst($node->left) . ' '
. $node->operator . ' '
. $node->right . '</a>';
$tpl = str_replace('{{FILTER_SUM}}', $sumText, $this->tpl);
$tpl = str_replace('{{REMOVE_FILTER}}', $url, $tpl);
if (count($node->right) > 1) {
$tpl = str_replace('{{SUBLIST}}', $this->getSubEntries($node), $tpl);
} else {
$tpl = str_replace('{{SUBLIST}}', '', $tpl);
}
return $tpl;
}
$result = '';
$result .= $this->nodeToBadge($node->left);
$this->conjunctionCellar = $node->type;
$result .= $this->nodeToBadge($node->right);

View File

@ -113,6 +113,7 @@ EOT;
$form->setIgnoreChangeDiscarding(true);
$badges = new FilterBadgeRenderer($this->initialFilter);
$html = str_replace('{{FORM}}', $form->render($view), self::$TPL);
$html = '<div class="input-append">' . $html . '</div>';
return str_replace('{{BADGES}}', $badges->render($view), $html);
}
}

View File

@ -99,7 +99,7 @@ class Registry implements FilterRegistry
$domain->registerAttribute(
FilterAttribute::create(new TextFilter())
->setHandledAttributes('Name', 'Hostname')
->setHandledAttributes('Name', 'Host', 'Hostname')
->setField('host_name')
)->registerAttribute(
FilterAttribute::create(StatusFilter::createForHost())
@ -173,6 +173,10 @@ class Registry implements FilterRegistry
FilterAttribute::create(self::getNextCheckFilterType())
->setHandledAttributes('Next Check')
->setField('service_next_check')
)->registerAttribute(
FilterAttribute::create(new TextFilter())
->setHandledAttributes('Hostname', 'Host')
->setField('host_name')
);
return $domain;
}
@ -211,11 +215,11 @@ class Registry implements FilterRegistry
parse_str($lastQuery, $lastParameters);
if ($lastFilter->root) {
$filter->insert($lastFilter->root);
$filter->insertTree($lastFilter);
}
$params = array();
foreach ($lastParameters as $key => $param) {
if (!$filter->hasNodeWithAttribute($key)) {
if (!$filter->hasNodeWithAttribute($key) && $view->isValidFilterTarget($key)) {
$params[$key] = $param;
}
}
@ -230,4 +234,9 @@ class Registry implements FilterRegistry
$urlString .= $urlParser->fromTree($filter);
return '/' . $urlString;
}
public function isValid($query)
{
}
}

View File

@ -60,7 +60,7 @@ class StatusFilter extends FilterType
'Is' => Node::OPERATOR_EQUALS,
'=' => Node::OPERATOR_EQUALS,
'!=' => Node::OPERATOR_EQUALS_NOT,
'Is not' => Node::OPERATOR_EQUALS_NOT
'Is Not' => Node::OPERATOR_EQUALS_NOT
);
/**
@ -214,12 +214,7 @@ class StatusFilter extends FilterType
private function getOperatorValueArray($query)
{
$result = array(null, null, null);
foreach ($this->getOperators() as $operator) {
if (stripos($query, $operator) === 0) {
$result[0] = $operator;
break;
}
}
$result[0] = self::getMatchingOperatorForQuery($query);
if ($result[0] === null) {
return $result;
}
@ -234,6 +229,7 @@ class StatusFilter extends FilterType
if ($result[2] && !$this->subFilter->isValidQuery($result[2])) {
return array(null, null, null);
}
return $result;
}

View File

@ -52,6 +52,9 @@ class UrlViewFilter
*/
private $target;
private $supportedConjunctions = array('&'/*, '|'*/);
/**
* Create a new ViewFilter
*
@ -77,9 +80,43 @@ class UrlViewFilter
if ($this->target) {
$filter = $filter->getCopyForFilterable($this->target);
}
$filter = $this->normalizeTreeNode($filter->root);
$filter->root = $filter->normalizeTree($filter->root);
return $this->convertNodeToUrlString($filter->root);
}
private function insertNormalizedOperatorNode($node, Tree $subTree = null)
{
$searchNode = $subTree->findNode(Node::createOperatorNode($node->operator, $node->left, null));
if ( $searchNode !== null) {
$result = array();
foreach ($node->right as $item) {
if (stripos($item, '*')) {
$subTree->insert(Node::createOperatorNode($node->operator, $node->left, $item));
} else {
$result = $result + $node->right;
}
}
$searchNode->right = array_merge($searchNode->right, $result);
} else {
$subTree->insert($node);
}
}
public function normalizeTreeNode($node, Tree $subTree = null)
{
$subTree = $subTree ? $subTree : new Tree();
if ($node->type === Node::TYPE_OPERATOR) {
$this->insertNormalizedOperatorNode($node, $subTree);
} else {
$subTree->insert($node->type === Node::TYPE_AND ? Node::createAndNode() : Node::createOrNode());
$subTree = $this->normalizeTreeNode($node->left, $subTree);
$subTree = $this->normalizeTreeNode($node->right, $subTree);
}
return $subTree;
}
/**
* Parse the given given url and return a query tree
*
@ -103,8 +140,8 @@ class UrlViewFilter
} elseif (is_array($token)) {
$tree->insert(
Node::createOperatorNode(
$token[self::FILTER_OPERATOR],
$token[self::FILTER_TARGET],
trim($token[self::FILTER_OPERATOR]),
trim($token[self::FILTER_TARGET]),
$token[self::FILTER_VALUE]
)
);
@ -133,12 +170,16 @@ class UrlViewFilter
{
$left = null;
$right = null;
if ($node->type === Node::TYPE_OPERATOR) {
if ($this->target && !$this->target->isValidFilterTarget($node->left)) {
return null;
}
return urlencode($node->left) . $node->operator . urlencode($node->right);
$values = array();
foreach ($node->right as $item) {
$values[] = urlencode($item);
}
return urlencode($node->left) . $node->operator . join(',', $values);
}
if ($node->left) {
$left = $this->convertNodeToUrlString($node->left);
@ -167,7 +208,7 @@ class UrlViewFilter
* array(
* self::FILTER_TARGET => 'Attribute',
* self::FILTER_OPERATOR => '!=',
* self::FILTER_VALUE => 'Value'
* self::FILTER_VALUE => array('Value')
* )
*
* @param String $query The query to tokenize
@ -233,7 +274,6 @@ class UrlViewFilter
*/
private function parseTarget($query, $currentPos, array &$tokenList)
{
$conjunctions = array('&', '|');
$i = $currentPos;
for ($i; $i < strlen($query); $i++) {
@ -254,7 +294,7 @@ class UrlViewFilter
}
// Implicit value token (test=1|2)
if (in_array($currentChar, $conjunctions) || $i + 1 == strlen($query)) {
if (in_array($currentChar, $this->supportedConjunctions) || $i + 1 == strlen($query)) {
$nrOfSymbols = count($tokenList);
if ($nrOfSymbols <= 2) {
return array($i, self::FILTER_TARGET);
@ -292,7 +332,6 @@ class UrlViewFilter
{
$i = $currentPos;
$conjunctions = array('&', '|');
$nrOfSymbols = count($tokenList);
if ($nrOfSymbols == 0) {
@ -301,7 +340,7 @@ class UrlViewFilter
$lastState = &$tokenList[$nrOfSymbols-1];
for ($i; $i < strlen($query); $i++) {
$currentChar = $query[$i];
if (in_array($currentChar, $conjunctions)) {
if (in_array($currentChar, $this->supportedConjunctions)) {
break;
}
}
@ -312,9 +351,9 @@ class UrlViewFilter
array_pop($tokenList);
return array($currentPos, self::FILTER_TARGET);
}
$lastState[self::FILTER_VALUE] = substr($query, $currentPos, $length);
$lastState[self::FILTER_VALUE] = explode(',', substr($query, $currentPos, $length));
if (in_array($currentChar, $conjunctions)) {
if (in_array($currentChar, $this->supportedConjunctions)) {
$tokenList[] = $currentChar;
}
return array($i, self::FILTER_TARGET);
@ -331,10 +370,9 @@ class UrlViewFilter
*/
private function skip($query, $currentPos)
{
$conjunctions = array('&', '|');
for ($i = $currentPos; strlen($query); $i++) {
$currentChar = $query[$i];
if (in_array($currentChar, $conjunctions)) {
if (in_array($currentChar, $this->supportedConjunctions)) {
return array($i, self::FILTER_TARGET);
}
}

View File

@ -93,7 +93,7 @@ define(['jquery', 'logging', 'URIjs/URI', 'components/app/container'], function(
* @param {String} state The HTTP state as a string
*/
this.showError = function(error, state) {
if (state === 'abort') {
if (!error.message || state === 'abort') {
return;
}
this.inputDom.popover('destroy').popover({
@ -136,7 +136,12 @@ define(['jquery', 'logging', 'URIjs/URI', 'components/app/container'], function(
return;
}
var list = $('<ul>').addClass('nav nav-stacked nav-pills');
if (response.valid) {
this.inputDom.parent('div').removeClass('has-error').addClass('has-success');
} else {
this.inputDom.parent('div').removeClass('has-success').addClass('has-error');
}
var list = $('<ul>').addClass('nav nav-stacked');
$.each(response.proposals, (function(idx, token) {
var displayToken = token.replace(/(\{|\})/g, '');
var proposal = $('<li>').
@ -165,7 +170,7 @@ define(['jquery', 'logging', 'URIjs/URI', 'components/app/container'], function(
*/
this.updateFilter = function() {
var query = $.trim(this.inputDom.val());
this.pendingRequest = $.ajax(this.getRequestParams(query))
$.ajax(this.getRequestParams(query))
.done((function(response) {
var domContainer = new Container(this.inputDom);
var url = response.urlParam;
@ -217,7 +222,7 @@ define(['jquery', 'logging', 'URIjs/URI', 'components/app/container'], function(
if (this.lastQueuedEvent) {
window.clearTimeout(this.lastQueuedEvent);
}
this.lastQueuedEvent = window.setTimeout(this.getProposal.bind(this), 200);
this.lastQueuedEvent = window.setTimeout(this.getProposal.bind(this), 500);
};
this.construct();