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:
Thomas Gelf 2014-06-17 12:28:28 +00:00
parent 5ea9b2be84
commit d1b2d47fed
13 changed files with 408 additions and 112 deletions

View File

@ -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

View File

@ -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';

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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) . ')';
}
}

View File

@ -2,7 +2,7 @@
namespace Icinga\Data\Filter;
class FilterOr extends FilterOperator
class FilterOr extends FilterChain
{
protected $operatorName = 'OR';

View File

@ -0,0 +1,9 @@
<?php
namespace Icinga\Data\Filter;
use Exception;
class FilterParseException extends Exception
{
}

View File

@ -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;
}
}