Data\Filter: rework fitting new URLs
A bunch of things happened here. We distinct FilterChains (or, and, not) from FilterExpressions (less, greater, equal...). We make use of our new URL-Parser. We can directly address anonymous filter components for editing filters. Too much things to explain them in detail, a filter documentation will follow.
This commit is contained in:
parent
5ea9b2be84
commit
d1b2d47fed
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace Icinga\Data\Filter;
|
||||
|
||||
class FilterEqual extends FilterExpression
|
||||
{
|
||||
public function matches($row)
|
||||
{
|
||||
return (string) $row->{$this->column} === (string) $this->expression;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace Icinga\Data\Filter;
|
||||
|
||||
class FilterEqualOrGreaterThan extends FilterExpression
|
||||
{
|
||||
public function matches($row)
|
||||
{
|
||||
return (string) $row->{$this->column} >= (string) $this->expression;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace Icinga\Data\Filter;
|
||||
|
||||
class FilterEqualOrLessThan extends FilterExpression
|
||||
{
|
||||
public function __toString()
|
||||
{
|
||||
return $this->column . ' <= ' . $this->expression;
|
||||
}
|
||||
|
||||
public function toQueryString()
|
||||
{
|
||||
return $this->column . '<=' . $this->expression;
|
||||
}
|
||||
|
||||
public function matches($row)
|
||||
{
|
||||
return (string) $row->{$this->column} <= (string) $this->expression;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
namespace Icinga\Data\Filter;
|
||||
|
||||
class FilterGreaterThan extends FilterExpression
|
||||
{
|
||||
|
||||
public function matches($row)
|
||||
{
|
||||
return (string) $row->{$this->column} > (string) $this->expression;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace Icinga\Data\Filter;
|
||||
|
||||
class FilterLessThan extends FilterExpression
|
||||
{
|
||||
public function __toString()
|
||||
{
|
||||
return $this->column . ' < ' . $this->expression;
|
||||
}
|
||||
|
||||
public function toQueryString()
|
||||
{
|
||||
return $this->column . '<' . $this->expression;
|
||||
}
|
||||
|
||||
public function matches($row)
|
||||
{
|
||||
return (string) $row->{$this->column} < (string) $this->expression;
|
||||
}
|
||||
}
|
|
@ -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) . ')';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace Icinga\Data\Filter;
|
||||
|
||||
class FilterOr extends FilterOperator
|
||||
class FilterOr extends FilterChain
|
||||
{
|
||||
protected $operatorName = 'OR';
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace Icinga\Data\Filter;
|
||||
|
||||
use Exception;
|
||||
|
||||
class FilterParseException extends Exception
|
||||
{
|
||||
}
|
|
@ -0,0 +1,240 @@
|
|||
<?php
|
||||
|
||||
namespace Icinga\Data\Filter;
|
||||
|
||||
class FilterQueryString
|
||||
{
|
||||
protected $string;
|
||||
|
||||
protected $pos;
|
||||
|
||||
protected $length;
|
||||
|
||||
protected function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public static function parse($string)
|
||||
{
|
||||
$parser = new static();
|
||||
return $parser->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 )<br>";
|
||||
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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue