diff --git a/library/Icinga/Data/Filter/Filter.php b/library/Icinga/Data/Filter/Filter.php index 49a78a2d2..78b06087b 100644 --- a/library/Icinga/Data/Filter/Filter.php +++ b/library/Icinga/Data/Filter/Filter.php @@ -2,6 +2,7 @@ namespace Icinga\Data\Filter; +use Icinga\Web\UrlParams; use Exception; /** @@ -9,7 +10,7 @@ use Exception; * * Base class for filters (why?) and factory for the different FilterOperators */ -class Filter +abstract class Filter { protected $id = '1'; @@ -19,6 +20,13 @@ class Filter return $this; } + abstract function toQueryString(); + + public function getUrlParams() + { + return UrlParams::fromQueryString($this->toQueryString()); + } + public function getById($id) { if ($id === $this->getId()) { @@ -53,7 +61,20 @@ class Filter */ public static function where($col, $filter) { - return new FilterWhere($col, $filter); + return new FilterExpression($col, '=', $filter); + } + + public static function expression($col, $op, $expression) + { + switch ($op) { + case '=': return new FilterEqual($col, $op, $expression); + case '<': return new FilterLessThan($col, $op, $expression); + case '>': return new FilterGreaterThan($col, $op, $expression); + case '>=': return new FilterEqualOrGreaterThan($col, $op, $expression); + case '<=': return new FilterEqualOrLessThan($col, $op, $expression); + case '!=': return new FilterNotEqual($col, $op, $expression); + default: throw new \Exception('WTTTTF'); + } } /** @@ -65,7 +86,11 @@ class Filter */ public static function matchAny() { - return new FilterOr(func_get_args()); + $args = func_get_args(); + if (count($args) === 1 && is_array($args[0])) { + $args = $args[0]; + } + return new FilterOr($args); } /** @@ -77,7 +102,11 @@ class Filter */ public static function matchAll() { - return new FilterAnd(func_get_args()); + $args = func_get_args(); + if (count($args) === 1 && is_array($args[0])) { + $args = $args[0]; + } + return new FilterAnd($args); } /** @@ -87,9 +116,15 @@ class Filter * * @return FilterNot */ - public static function not(Filter $filter) + public static function not() { - return new FilterNot(array($filter)); // ?? + $args = func_get_args(); + if (count($args) === 1) { + if (is_array($args[0])) { + $args = $args[0]; + } + } + return new FilterNot($args); } /** @@ -99,82 +134,9 @@ class Filter */ public static function fromQueryString($query) { - $query = rawurldecode($query); - $parts = preg_split('~&~', $query, -1, PREG_SPLIT_NO_EMPTY); - $filters = Filter::matchAll(); - foreach ($parts as $part) { - self::parseQueryStringPart($part, $filters); - } - return $filters; + return FilterQueryString::parse($query); } - /** - * Parse query string part - * - */ - protected static function parseQueryStringPart($part, & $filters) - { - $negations = 0; - - if (strpos($part, '=') === false) { - - $key = rawurldecode($part); - - while (substr($key, 0, 1) === '!') { - if (strlen($key) < 2) { - throw new FilterException( - sprintf('Got invalid filter part: "%s"', $part) - ); - } - $key = substr($key, 1); - $negations++; - } - - $filter = Filter::where($key, true); - - } else { - list($key, $val) = preg_split('/=/', $part, 2); - $key = rawurldecode($key); - $val = rawurldecode($val); - - while (substr($key, 0, 1) === '!') { - if (strlen($key) < 2) { - throw new FilterException( - sprintf('Got invalid filter part: "%s"', $part) - ); - } - $key = substr($key, 1); - $negations++; - } - - while (substr($key, -1) === '!') { - if (strlen($key) < 2) { - throw new FilterException( - sprintf('Got invalid filter part: "%s"', $part) - ); - } - $key = substr($key, 0, -1); - $negations++; - } - - if (strpos($val, '|') !== false) { - $vals = preg_split('/\|/', $val, -1, PREG_SPLIT_NO_EMPTY); - $filter = Filter::matchAny(); - foreach ($vals as $val) { - $filter->addFilter(Filter::where($key, $val)); - } - } else { - $filter = Filter::where($key, $val); - } - - } - - if ($negations % 2 === 0) { - $filters->addFilter($filter); - } else { - $filters->addFilter(Filter::not($filter)); - } - } /** * We need a new Querystring-Parser diff --git a/library/Icinga/Data/Filter/FilterAnd.php b/library/Icinga/Data/Filter/FilterAnd.php index 02c0f12a9..61fabdb0c 100644 --- a/library/Icinga/Data/Filter/FilterAnd.php +++ b/library/Icinga/Data/Filter/FilterAnd.php @@ -7,7 +7,7 @@ namespace Icinga\Data\Filter; * * Binary AND, all contained filters must succeed */ -class FilterAnd extends FilterOperator +class FilterAnd extends FilterChain { protected $operatorName = 'AND'; diff --git a/library/Icinga/Data/Filter/FilterOperator.php b/library/Icinga/Data/Filter/FilterChain.php similarity index 93% rename from library/Icinga/Data/Filter/FilterOperator.php rename to library/Icinga/Data/Filter/FilterChain.php index be7ee3f0b..4d459b1d1 100644 --- a/library/Icinga/Data/Filter/FilterOperator.php +++ b/library/Icinga/Data/Filter/FilterChain.php @@ -3,11 +3,11 @@ namespace Icinga\Data\Filter; /** - * FilterOperator + * FilterChain * - * A FilterOperator contains a list ... + * A FilterChain contains a list ... */ -abstract class FilterOperator extends Filter +abstract class FilterChain extends Filter { protected $filters = array(); @@ -45,7 +45,7 @@ abstract class FilterOperator extends Filter foreach ($this->filters as $key => $filter) { if ($filter->getId() === $id) { $remove = $key; - } elseif ($filter instanceof FilterOperator) { + } elseif ($filter instanceof FilterChain) { $filter->removeId($id); } } @@ -115,7 +115,7 @@ abstract class FilterOperator extends Filter } $parts = array(); foreach ($this->filters as $filter) { - if ($filter instanceof FilterOperator) { + if ($filter instanceof FilterChain) { $parts[] = '(' . $filter . ')'; } else { $parts[] = (string) $filter; diff --git a/library/Icinga/Data/Filter/FilterEqual.php b/library/Icinga/Data/Filter/FilterEqual.php new file mode 100644 index 000000000..5b16b37a1 --- /dev/null +++ b/library/Icinga/Data/Filter/FilterEqual.php @@ -0,0 +1,11 @@ +{$this->column} === (string) $this->expression; + } +} diff --git a/library/Icinga/Data/Filter/FilterEqualOrGreaterThan.php b/library/Icinga/Data/Filter/FilterEqualOrGreaterThan.php new file mode 100644 index 000000000..e57541e71 --- /dev/null +++ b/library/Icinga/Data/Filter/FilterEqualOrGreaterThan.php @@ -0,0 +1,11 @@ +{$this->column} >= (string) $this->expression; + } +} diff --git a/library/Icinga/Data/Filter/FilterEqualOrLessThan.php b/library/Icinga/Data/Filter/FilterEqualOrLessThan.php new file mode 100644 index 000000000..47b6d1906 --- /dev/null +++ b/library/Icinga/Data/Filter/FilterEqualOrLessThan.php @@ -0,0 +1,21 @@ +column . ' <= ' . $this->expression; + } + + public function toQueryString() + { + return $this->column . '<=' . $this->expression; + } + + public function matches($row) + { + return (string) $row->{$this->column} <= (string) $this->expression; + } +} diff --git a/library/Icinga/Data/Filter/FilterWhere.php b/library/Icinga/Data/Filter/FilterExpression.php similarity index 60% rename from library/Icinga/Data/Filter/FilterWhere.php rename to library/Icinga/Data/Filter/FilterExpression.php index 16a73d9f8..52c70d19b 100644 --- a/library/Icinga/Data/Filter/FilterWhere.php +++ b/library/Icinga/Data/Filter/FilterExpression.php @@ -2,22 +2,34 @@ namespace Icinga\Data\Filter; -class FilterWhere extends Filter +class FilterExpression extends Filter { protected $column; + protected $sign; protected $expression; - public function __construct($column, $expression) + public function __construct($column, $sign, $expression) { $this->column = $column; + $this->sign = $sign; $this->expression = $expression; } + public function isEmpty() + { + return false; + } + public function getColumn() { return $this->column; } + public function getSign() + { + return $this->sign; + } + public function setColumn($column) { $this->column = $column; @@ -31,20 +43,25 @@ class FilterWhere extends Filter public function __toString() { - if (is_array($this->expression)) { - return $this->column . ' = ( ' . implode(' | ', $this->expression) . ' )'; - } else { - return $this->column . ' = ' . $this->expression; - } + $expression = is_array($this->expression) ? + '( ' . implode(' | ', $this->expression) . ' )' : + $this->expression; + + return sprintf( + '%s %s %s', + $this->column, + $this->sign, + $expression + ); } public function toQueryString() { - if (is_array($this->expression)) { - return $this->column . '=' . implode('|', $this->expression); - } else { - return $this->column . '=' . $this->expression; - } + $expression = is_array($this->expression) ? + '(' . implode('|', $this->expression) . ')' : + $this->expression; + + return $this->column . $this->sign . $expression; } public function matches($row) @@ -61,12 +78,5 @@ class FilterWhere extends Filter $pattern = '/^' . implode('.*', $parts) . '$/'; return (bool) preg_match($pattern, $row->{$this->column}); } - - foreach ($this->filters as $filter) { - if (! $filter->matches($row)) { - return false; - } - } - return true; } } diff --git a/library/Icinga/Data/Filter/FilterGreaterThan.php b/library/Icinga/Data/Filter/FilterGreaterThan.php new file mode 100644 index 000000000..6fd4f2b2d --- /dev/null +++ b/library/Icinga/Data/Filter/FilterGreaterThan.php @@ -0,0 +1,12 @@ +{$this->column} > (string) $this->expression; + } +} diff --git a/library/Icinga/Data/Filter/FilterLessThan.php b/library/Icinga/Data/Filter/FilterLessThan.php new file mode 100644 index 000000000..dabccc669 --- /dev/null +++ b/library/Icinga/Data/Filter/FilterLessThan.php @@ -0,0 +1,21 @@ +column . ' < ' . $this->expression; + } + + public function toQueryString() + { + return $this->column . '<' . $this->expression; + } + + public function matches($row) + { + return (string) $row->{$this->column} < (string) $this->expression; + } +} diff --git a/library/Icinga/Data/Filter/FilterNot.php b/library/Icinga/Data/Filter/FilterNot.php index cbb66b407..39131b671 100644 --- a/library/Icinga/Data/Filter/FilterNot.php +++ b/library/Icinga/Data/Filter/FilterNot.php @@ -2,7 +2,7 @@ namespace Icinga\Data\Filter; -class FilterNot extends FilterOperator +class FilterNot extends FilterChain { protected $operatorName = 'NOT'; @@ -26,6 +26,7 @@ class FilterNot extends FilterOperator if (empty($this->filters)) { return ''; } + foreach ($this->filters() as $filter) { $parts[] = $filter->toQueryString(); } @@ -38,11 +39,9 @@ class FilterNot extends FilterOperator public function __toString() { - $sub = Filter::matchAll(); - var_dump($this->filters()); - foreach ($this->filters() as $f) { - $sub->addFilter($f); + if (count($this->filters) === 1) { + return '! ' . $this->filters[0]; } - return '! (' . $sub . ')'; + return '! (' . implode('&', $this->filters) . ')'; } } diff --git a/library/Icinga/Data/Filter/FilterOr.php b/library/Icinga/Data/Filter/FilterOr.php index 8531f66bf..c8cd40b70 100644 --- a/library/Icinga/Data/Filter/FilterOr.php +++ b/library/Icinga/Data/Filter/FilterOr.php @@ -2,7 +2,7 @@ namespace Icinga\Data\Filter; -class FilterOr extends FilterOperator +class FilterOr extends FilterChain { protected $operatorName = 'OR'; diff --git a/library/Icinga/Data/Filter/FilterParseException.php b/library/Icinga/Data/Filter/FilterParseException.php new file mode 100644 index 000000000..b56c6ba67 --- /dev/null +++ b/library/Icinga/Data/Filter/FilterParseException.php @@ -0,0 +1,9 @@ +parseQueryString($string); + } + + protected function readNextKey() + { + $str = $this->readUnlessSpecialChar(); + + if ($str === false) { + return $str; + } + return rawurldecode($str); + } + + protected function readNextValue() + { + $var = rawurldecode($this->readUnlessSpecialChar()); + if ($var === '' && $this->nextChar() === '(') { + $this->readChar(); + $var = preg_split('~\|~', $this->readUnless(')')); + if ($this->readChar() !== ')') { + $this->parseError(null, 'Expected ")"'); + } + } + return $var; + } + + protected function readNextExpression() + { + + if ('' === ($key = $this->readNextKey())) { + return false; + } + + $sign = $this->readChar(); + if ($sign === false) { + return Filter::expression($key, '=', true); + } + + if ($sign === '=') { + $last = substr($key, -1); + if ($last === '>' || $last === '<') { + $sign = $last . $sign; + $key = substr($key, 0, -1); + } + // TODO: Same as above for unescaped <> - do we really need this? + } elseif ($sign === '>' || $sign === '<' || $sign === '!') { + if ($this->nextChar() === '=') { + $sign .= $this->readChar(); + } + } + + $var = $this->readNextValue(); + + return Filter::expression($key, $sign, $var); + } + + protected function parseError($char = null, $extraMsg = null) + { + if ($extraMsg === null) { + $extra = ''; + } else { + $extra = ': ' . $extraMsg; + } + if ($char === null) { + $char = $this->string[$this->pos]; + } + + throw new FilterParseException(sprintf( + 'Invalid filter "%s", unexpected %s at pos %d%s', + $this->string, + $char, + $this->pos, + $extra + )); + } + + protected function readFilters($nestingLevel = 0, $op = '&') + { + $filters = array(); + while ($this->pos < $this->length) { + + if ($op === '!' && count($filters) === 1) { + break; + } + $filter = $this->readNextExpression(); + $next = $this->readChar(); + + + if ($filter === false) { + + if ($next === '!') { + $not = $this->readFilters($nestingLevel + 1, '!'); + $filters[] = $not; + continue; + } + + if ($next === false) { + // Nothing more to read + break; + } + + if ($next === ')') { + if ($nestingLevel > 0) { + break; + } + $this->parseError($next); + } + if ($next === '(') { + $filters[] = $this->readFilters($nestingLevel + 1, null); + continue; + } + if ($next === $op) { + continue; + } + $this->parseError($next, "$op level $nestingLevel"); + + } else { + $filters[] = $filter; + + if ($next === false) { + // Got filter, nothing more to read + break; + } + + if ($op === '!') { + $this->pos--; + break; + } + if ($next === $op) { + continue; // Break?? + } + + if ($next === ')') { + if ($nestingLevel > 0) { + echo "Got )
"; + break; + } + $this->parseError($next); + } + if ($op === null && in_array($next, array('&', '|'))) { + $op = $next; + continue; + } + $this->parseError($next); + } + + } + + if ($nestingLevel === 0 && count($filters) === 1) { + // There is only one filter expression, no chain + return $filters[0]; + } + if ($op === null && count($filters) === 1) { + $op = '&'; + } + + switch ($op) { + case '&': return Filter::matchAll($filters); + case '|': return Filter::matchAny($filters); + case '!': return Filter::not($filters); + default: $this->parseError($op); + } + } + + protected function parseQueryString($string) + { + $this->pos = 0; + + $this->string = $string; + + $this->length = strlen($string); + return $this->readFilters(); + } + + protected function readUnless($char) + { + $buffer = ''; + while (false !== ($c = $this->readChar())) { + if (is_array($char)) { + if (in_array($c, $char)) { + $this->pos--; + break; + } + } else { + if ($c === $char) { + $this->pos--; + break; + } + } + $buffer .= $c; + } + + return $buffer; + } + + protected function readUnlessSpecialChar() + { + return $this->readUnless(array('=', '(', ')', '&', '|', '>', '<', '!')); + } + + protected function readExpressionOperator() + { + return $this->readUnless(array('=', '>', '<', '!')); + } + + protected function readChar() + { + if ($this->length > $this->pos) { + return $this->string[$this->pos++]; + } + return false; + } + + protected function nextChar() + { + if ($this->length > $this->pos) { + return $this->string[$this->pos]; + } + return false; + } +}