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; namespace Icinga\Data\Filter;
use Icinga\Web\UrlParams;
use Exception; use Exception;
/** /**
@ -9,7 +10,7 @@ use Exception;
* *
* Base class for filters (why?) and factory for the different FilterOperators * Base class for filters (why?) and factory for the different FilterOperators
*/ */
class Filter abstract class Filter
{ {
protected $id = '1'; protected $id = '1';
@ -19,6 +20,13 @@ class Filter
return $this; return $this;
} }
abstract function toQueryString();
public function getUrlParams()
{
return UrlParams::fromQueryString($this->toQueryString());
}
public function getById($id) public function getById($id)
{ {
if ($id === $this->getId()) { if ($id === $this->getId()) {
@ -53,7 +61,20 @@ class Filter
*/ */
public static function where($col, $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() 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() 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 * @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) public static function fromQueryString($query)
{ {
$query = rawurldecode($query); return FilterQueryString::parse($query);
$parts = preg_split('~&~', $query, -1, PREG_SPLIT_NO_EMPTY);
$filters = Filter::matchAll();
foreach ($parts as $part) {
self::parseQueryStringPart($part, $filters);
}
return $filters;
} }
/**
* 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 * We need a new Querystring-Parser

View File

@ -7,7 +7,7 @@ namespace Icinga\Data\Filter;
* *
* Binary AND, all contained filters must succeed * Binary AND, all contained filters must succeed
*/ */
class FilterAnd extends FilterOperator class FilterAnd extends FilterChain
{ {
protected $operatorName = 'AND'; protected $operatorName = 'AND';

View File

@ -3,11 +3,11 @@
namespace Icinga\Data\Filter; 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(); protected $filters = array();
@ -45,7 +45,7 @@ abstract class FilterOperator extends Filter
foreach ($this->filters as $key => $filter) { foreach ($this->filters as $key => $filter) {
if ($filter->getId() === $id) { if ($filter->getId() === $id) {
$remove = $key; $remove = $key;
} elseif ($filter instanceof FilterOperator) { } elseif ($filter instanceof FilterChain) {
$filter->removeId($id); $filter->removeId($id);
} }
} }
@ -115,7 +115,7 @@ abstract class FilterOperator extends Filter
} }
$parts = array(); $parts = array();
foreach ($this->filters as $filter) { foreach ($this->filters as $filter) {
if ($filter instanceof FilterOperator) { if ($filter instanceof FilterChain) {
$parts[] = '(' . $filter . ')'; $parts[] = '(' . $filter . ')';
} else { } else {
$parts[] = (string) $filter; $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; namespace Icinga\Data\Filter;
class FilterWhere extends Filter class FilterExpression extends Filter
{ {
protected $column; protected $column;
protected $sign;
protected $expression; protected $expression;
public function __construct($column, $expression) public function __construct($column, $sign, $expression)
{ {
$this->column = $column; $this->column = $column;
$this->sign = $sign;
$this->expression = $expression; $this->expression = $expression;
} }
public function isEmpty()
{
return false;
}
public function getColumn() public function getColumn()
{ {
return $this->column; return $this->column;
} }
public function getSign()
{
return $this->sign;
}
public function setColumn($column) public function setColumn($column)
{ {
$this->column = $column; $this->column = $column;
@ -31,20 +43,25 @@ class FilterWhere extends Filter
public function __toString() public function __toString()
{ {
if (is_array($this->expression)) { $expression = is_array($this->expression) ?
return $this->column . ' = ( ' . implode(' | ', $this->expression) . ' )'; '( ' . implode(' | ', $this->expression) . ' )' :
} else { $this->expression;
return $this->column . ' = ' . $this->expression;
} return sprintf(
'%s %s %s',
$this->column,
$this->sign,
$expression
);
} }
public function toQueryString() public function toQueryString()
{ {
if (is_array($this->expression)) { $expression = is_array($this->expression) ?
return $this->column . '=' . implode('|', $this->expression); '(' . implode('|', $this->expression) . ')' :
} else { $this->expression;
return $this->column . '=' . $this->expression;
} return $this->column . $this->sign . $expression;
} }
public function matches($row) public function matches($row)
@ -61,12 +78,5 @@ class FilterWhere extends Filter
$pattern = '/^' . implode('.*', $parts) . '$/'; $pattern = '/^' . implode('.*', $parts) . '$/';
return (bool) preg_match($pattern, $row->{$this->column}); 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; namespace Icinga\Data\Filter;
class FilterNot extends FilterOperator class FilterNot extends FilterChain
{ {
protected $operatorName = 'NOT'; protected $operatorName = 'NOT';
@ -26,6 +26,7 @@ class FilterNot extends FilterOperator
if (empty($this->filters)) { if (empty($this->filters)) {
return ''; return '';
} }
foreach ($this->filters() as $filter) { foreach ($this->filters() as $filter) {
$parts[] = $filter->toQueryString(); $parts[] = $filter->toQueryString();
} }
@ -38,11 +39,9 @@ class FilterNot extends FilterOperator
public function __toString() public function __toString()
{ {
$sub = Filter::matchAll(); if (count($this->filters) === 1) {
var_dump($this->filters()); return '! ' . $this->filters[0];
foreach ($this->filters() as $f) {
$sub->addFilter($f);
} }
return '! (' . $sub . ')'; return '! (' . implode('&', $this->filters) . ')';
} }
} }

View File

@ -2,7 +2,7 @@
namespace Icinga\Data\Filter; namespace Icinga\Data\Filter;
class FilterOr extends FilterOperator class FilterOr extends FilterChain
{ {
protected $operatorName = 'OR'; 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;
}
}