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

View File

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

View File

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

View File

@ -113,6 +113,14 @@ class Node
$node->type = self::TYPE_OPERATOR; $node->type = self::TYPE_OPERATOR;
$node->operator = $operator; $node->operator = $operator;
$node->left = $left; $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; $node->right = $right;
return $node; return $node;
} }

View File

@ -54,6 +54,23 @@ class Tree
*/ */
private $lastNode; 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 * Insert a node into this tree, recognizing type and insert position
* *
@ -77,7 +94,6 @@ class Tree
$this->insert(Node::createAndNode()); $this->insert(Node::createAndNode());
} }
$node->parent = $this->lastNode; $node->parent = $this->lastNode;
if ($this->lastNode->left == null) { if ($this->lastNode->left == null) {
$this->lastNode->left = $node; $this->lastNode->left = $node;
} elseif ($this->lastNode->right == null) { } elseif ($this->lastNode->right == null) {
@ -86,6 +102,7 @@ class Tree
break; break;
} }
} }
$this->lastNode = $node; $this->lastNode = $node;
} }
@ -350,16 +367,23 @@ class Tree
if ($ctx === null) { if ($ctx === null) {
return null; return null;
} }
if ($ctx->type === Node::TYPE_OPERATOR) {
if ($ctx->left == $node->left && $ctx->right == $node->right && $ctx->operator == $node->operator) { if ($ctx->type == Node::TYPE_OPERATOR) {
return $ctx; if ($ctx->left == $node->left && $ctx->operator == $node->operator) {
if(empty($node->right) || $ctx->right == $node->right) {
return $ctx;
}
} }
return null; return null;
} else { } else {
$result = $this->findNode($node, $ctx->left); $result = null;
if ($result === null) { if ($ctx->left) {
$result = $this->findNode($node, $ctx->left);
} if ($result == null && $ctx->right) {
$result = $this->findNode($node, $ctx->right); $result = $this->findNode($node, $ctx->right);
} }
return $result; return $result;
} }
} }
@ -367,21 +391,25 @@ class Tree
/** /**
* Return true if A node with the given attribute on the left side exists * Return true if A node with the given attribute on the left side exists
* *
* @param String $name The attribute to test for existence * @param String $name The attribute to test for existence
* @param Node $ctx The current root node * @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 * @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) { if ($ctx === null) {
return false; return false;
} }
if ($ctx->type === Node::TYPE_OPERATOR) { if ($ctx->type === Node::TYPE_OPERATOR) {
return $ctx->left === $name; return $ctx->left === $name;
} else { } 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]); $indexes = array_keys($state[$target]);
if ($baseGroup) { if ($baseGroup) {
$baseGroup->setQuery($this); $baseGroup->setQuery($this);
$indexes = $baseGroup->filter($state[$target]); $idx = array_keys($state[$target]);
$indexes = $baseGroup->filter($state[$target], $idx );
} }
if (!isset($result[$target])) { if (!isset($result[$target])) {
$result[$target] = $indexes; $result[$target] = $indexes;

View File

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

View File

@ -26,30 +26,48 @@
*/ */
// {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Protocol\Statusdat; namespace Icinga\Protocol\Statusdat;
use Icinga\Filter\Filterable; use Icinga\Filter\Filterable;
use Icinga\Filter\Query\Node; use Icinga\Filter\Query\Node;
use Icinga\Filter\Query\Tree; use Icinga\Filter\Query\Tree;
use Icinga\Protocol\Statusdat\Query\Expression; use Icinga\Protocol\Statusdat\Query\Expression;
use Icinga\Protocol\Statusdat\Query\Group; use Icinga\Protocol\Statusdat\Query\Group;
use Icinga\Protocol\Statusdat\Query\IQueryPart;
/**
* Parser to create statusdat filter expressions from query trees
*
*/
class TreeToStatusdatQueryParser 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) { if ($node->type === Node::TYPE_OPERATOR) {
$op = $node->operator; $op = $node->operator;
$value = $node->right;
$node->left = $source->getMappedField($node->left); $node->left = $source->getMappedField($node->left);
if (stripos($node->right, '*') !== false) { $op = 'IN';
$op = 'LIKE'; $values = $node->right;
if ($node->operator === NODE::OPERATOR_EQUALS_NOT) {
$op = 'NOT_' . $op;
}
foreach ($values as &$value) {
$value = str_replace('*', '%', $value); $value = str_replace('*', '%', $value);
} }
return new Expression($node->left . ' ' . $op . ' ?', $value); $values = array($values);
return new Expression($node->left . ' ' . $op . ' ? ', $values);
} else { } else {
$group = new Group(); $group = new Group();
$group->setType(($node->type === Node::TYPE_OR) ? Group::TYPE_OR : Group::TYPE_AND); $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) public function treeToQuery(Tree $tree, Filterable $source)
{ {
@ -70,4 +96,4 @@ class TreeToStatusdatQueryParser
} }
return null; return null;
} }
} }

View File

@ -48,6 +48,29 @@ class FilterBadgeRenderer implements Widget
private $conjunctionCellar = ''; private $conjunctionCellar = '';
private $urlFilter; 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 * Create a new badge renderer for this tree
* *
@ -58,6 +81,41 @@ class FilterBadgeRenderer implements Widget
$this->tree = $tree; $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 * Create a removable badge from a query tree node
* *
@ -74,15 +132,18 @@ class FilterBadgeRenderer implements Widget
$newTree = $this->tree->withoutNode($node); $newTree = $this->tree->withoutNode($node);
$url = $this->urlFilter->fromTree($newTree); $url = $this->urlFilter->fromTree($newTree);
$url = $basePath . (empty($allParams) ? '?' : '&') . $url; $url = $basePath . (empty($allParams) ? '?' : '&') . $url;
$sumText = $this->getSummarizedText($node);
return ' <a class="filter-badge btn btn-default btn-xs" href="' . $url . '">' $tpl = str_replace('{{FILTER_SUM}}', $sumText, $this->tpl);
. $this->conjunctionCellar . ' ' $tpl = str_replace('{{REMOVE_FILTER}}', $url, $tpl);
. ucfirst($node->left) . ' ' if (count($node->right) > 1) {
. $node->operator . ' ' $tpl = str_replace('{{SUBLIST}}', $this->getSubEntries($node), $tpl);
. $node->right . '</a>'; } else {
$tpl = str_replace('{{SUBLIST}}', '', $tpl);
}
return $tpl;
} }
$result = ''; $result = '';
$result .= $this->nodeToBadge($node->left); $result .= $this->nodeToBadge($node->left);
$this->conjunctionCellar = $node->type; $this->conjunctionCellar = $node->type;
$result .= $this->nodeToBadge($node->right); $result .= $this->nodeToBadge($node->right);

View File

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

View File

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

View File

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

View File

@ -52,6 +52,9 @@ class UrlViewFilter
*/ */
private $target; private $target;
private $supportedConjunctions = array('&'/*, '|'*/);
/** /**
* Create a new ViewFilter * Create a new ViewFilter
* *
@ -77,9 +80,43 @@ class UrlViewFilter
if ($this->target) { if ($this->target) {
$filter = $filter->getCopyForFilterable($this->target); $filter = $filter->getCopyForFilterable($this->target);
} }
$filter = $this->normalizeTreeNode($filter->root);
$filter->root = $filter->normalizeTree($filter->root);
return $this->convertNodeToUrlString($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 * Parse the given given url and return a query tree
* *
@ -103,8 +140,8 @@ class UrlViewFilter
} elseif (is_array($token)) { } elseif (is_array($token)) {
$tree->insert( $tree->insert(
Node::createOperatorNode( Node::createOperatorNode(
$token[self::FILTER_OPERATOR], trim($token[self::FILTER_OPERATOR]),
$token[self::FILTER_TARGET], trim($token[self::FILTER_TARGET]),
$token[self::FILTER_VALUE] $token[self::FILTER_VALUE]
) )
); );
@ -133,12 +170,16 @@ class UrlViewFilter
{ {
$left = null; $left = null;
$right = null; $right = null;
if ($node->type === Node::TYPE_OPERATOR) { if ($node->type === Node::TYPE_OPERATOR) {
if ($this->target && !$this->target->isValidFilterTarget($node->left)) { if ($this->target && !$this->target->isValidFilterTarget($node->left)) {
return null; 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) { if ($node->left) {
$left = $this->convertNodeToUrlString($node->left); $left = $this->convertNodeToUrlString($node->left);
@ -167,7 +208,7 @@ class UrlViewFilter
* array( * array(
* self::FILTER_TARGET => 'Attribute', * self::FILTER_TARGET => 'Attribute',
* self::FILTER_OPERATOR => '!=', * self::FILTER_OPERATOR => '!=',
* self::FILTER_VALUE => 'Value' * self::FILTER_VALUE => array('Value')
* ) * )
* *
* @param String $query The query to tokenize * @param String $query The query to tokenize
@ -233,7 +274,6 @@ class UrlViewFilter
*/ */
private function parseTarget($query, $currentPos, array &$tokenList) private function parseTarget($query, $currentPos, array &$tokenList)
{ {
$conjunctions = array('&', '|');
$i = $currentPos; $i = $currentPos;
for ($i; $i < strlen($query); $i++) { for ($i; $i < strlen($query); $i++) {
@ -254,7 +294,7 @@ class UrlViewFilter
} }
// Implicit value token (test=1|2) // 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); $nrOfSymbols = count($tokenList);
if ($nrOfSymbols <= 2) { if ($nrOfSymbols <= 2) {
return array($i, self::FILTER_TARGET); return array($i, self::FILTER_TARGET);
@ -292,7 +332,6 @@ class UrlViewFilter
{ {
$i = $currentPos; $i = $currentPos;
$conjunctions = array('&', '|');
$nrOfSymbols = count($tokenList); $nrOfSymbols = count($tokenList);
if ($nrOfSymbols == 0) { if ($nrOfSymbols == 0) {
@ -301,7 +340,7 @@ class UrlViewFilter
$lastState = &$tokenList[$nrOfSymbols-1]; $lastState = &$tokenList[$nrOfSymbols-1];
for ($i; $i < strlen($query); $i++) { for ($i; $i < strlen($query); $i++) {
$currentChar = $query[$i]; $currentChar = $query[$i];
if (in_array($currentChar, $conjunctions)) { if (in_array($currentChar, $this->supportedConjunctions)) {
break; break;
} }
} }
@ -312,9 +351,9 @@ class UrlViewFilter
array_pop($tokenList); array_pop($tokenList);
return array($currentPos, self::FILTER_TARGET); 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; $tokenList[] = $currentChar;
} }
return array($i, self::FILTER_TARGET); return array($i, self::FILTER_TARGET);
@ -331,10 +370,9 @@ class UrlViewFilter
*/ */
private function skip($query, $currentPos) private function skip($query, $currentPos)
{ {
$conjunctions = array('&', '|');
for ($i = $currentPos; strlen($query); $i++) { for ($i = $currentPos; strlen($query); $i++) {
$currentChar = $query[$i]; $currentChar = $query[$i];
if (in_array($currentChar, $conjunctions)) { if (in_array($currentChar, $this->supportedConjunctions)) {
return array($i, self::FILTER_TARGET); 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 * @param {String} state The HTTP state as a string
*/ */
this.showError = function(error, state) { this.showError = function(error, state) {
if (state === 'abort') { if (!error.message || state === 'abort') {
return; return;
} }
this.inputDom.popover('destroy').popover({ this.inputDom.popover('destroy').popover({
@ -136,7 +136,12 @@ define(['jquery', 'logging', 'URIjs/URI', 'components/app/container'], function(
return; 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) { $.each(response.proposals, (function(idx, token) {
var displayToken = token.replace(/(\{|\})/g, ''); var displayToken = token.replace(/(\{|\})/g, '');
var proposal = $('<li>'). var proposal = $('<li>').
@ -165,7 +170,7 @@ define(['jquery', 'logging', 'URIjs/URI', 'components/app/container'], function(
*/ */
this.updateFilter = function() { this.updateFilter = function() {
var query = $.trim(this.inputDom.val()); var query = $.trim(this.inputDom.val());
this.pendingRequest = $.ajax(this.getRequestParams(query)) $.ajax(this.getRequestParams(query))
.done((function(response) { .done((function(response) {
var domContainer = new Container(this.inputDom); var domContainer = new Container(this.inputDom);
var url = response.urlParam; var url = response.urlParam;
@ -217,7 +222,7 @@ define(['jquery', 'logging', 'URIjs/URI', 'components/app/container'], function(
if (this.lastQueuedEvent) { if (this.lastQueuedEvent) {
window.clearTimeout(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(); this.construct();