Semantic search implementation

- Only implemented for hosts as an example
- URL behaviour still has to be normalized

refs #4469
This commit is contained in:
Jannis Moßhammer 2013-10-14 13:25:25 +02:00
parent dac61eda19
commit d33cec78de
36 changed files with 1540 additions and 784 deletions

View File

@ -25,6 +25,7 @@
* @author Icinga Development Team <info@icinga.org> * @author Icinga Development Team <info@icinga.org>
*/ */
// {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}}
// @codingStandardsIgnoreStart
use Icinga\Web\Form; use Icinga\Web\Form;
use Icinga\Web\Controller\ActionController; use Icinga\Web\Controller\ActionController;
@ -34,69 +35,82 @@ use Icinga\Filter\Type\TextFilter;
use Icinga\Application\Logger; use Icinga\Application\Logger;
use Icinga\Module\Monitoring\Filter\Type\StatusFilter; use Icinga\Module\Monitoring\Filter\Type\StatusFilter;
use Icinga\Module\Monitoring\Filter\UrlViewFilter; use Icinga\Module\Monitoring\Filter\UrlViewFilter;
use Icinga\Module\Monitoring\DataView\HostStatus;
use Icinga\Web\Url; use Icinga\Web\Url;
/**
* Application wide interface for filtering
*/
class FilterController extends ActionController class FilterController extends ActionController
{ {
/** /**
* The current filter registry
*
* @var Filter * @var Filter
*/ */
private $registry; private $registry;
/**
* Entry point for filtering, uses the filter_domain and filter_module request parameter
* to determine which filter registry should be used
*/
public function indexAction() public function indexAction()
{ {
$this->registry = new Filter(); $this->registry = new Filter();
$filter = new UrlViewFilter();
$this->view->form = new Form(); if ($this->getRequest()->getHeader('accept') == 'application/json') {
$this->view->form->addElement(
'text',
'query',
array(
'name' => 'query',
'label' => 'search',
'type' => 'search',
'data-icinga-component' => 'app/semanticsearch',
'data-icinga-target' => 'host',
'helptext' => 'Filter test'
)
);
$this->view->form->addElement(
'submit',
'btn_submit',
array(
'name' => 'submit'
)
);
$this->setupQueries();
$this->view->form->setRequest($this->getRequest());
if ($this->view->form->isSubmittedAndValid()) {
$tree = $this->registry->createQueryTreeForFilter($this->view->form->getValue('query'));
$this->view->tree = new \Icinga\Web\Widget\FilterBadgeRenderer($tree);
$view = \Icinga\Module\Monitoring\DataView\HostAndServiceStatus::fromRequest($this->getRequest());
$cv = new \Icinga\Module\Monitoring\Filter\Backend\IdoQueryConverter($view);
$this->view->sqlString = $cv->treeToSql($tree);
$this->view->params = $cv->getParams();
} else if ($this->getRequest()->getHeader('accept') == 'application/json') {
$this->getResponse()->setHeader('Content-Type', 'application/json'); $this->getResponse()->setHeader('Content-Type', 'application/json');
$this->setupQueries(
$this->getParam('filter_domain', ''),
$this->getParam('filter_module', '')
);
$this->_helper->json($this->parse($this->getRequest()->getParam('query', ''))); $this->_helper->json($this->parse($this->getRequest()->getParam('query', '')));
} else {
$this->redirect('index/welcome');
} }
} }
private function setupQueries() /**
* Set up the query handler for the given domain and module
*
* @param string $domain The domain to use
* @param string $module The module to use
*/
private function setupQueries($domain, $module = 'default')
{ {
$this->registry->addDomain(\Icinga\Module\Monitoring\Filter\MonitoringFilter::hostFilter()); $class = '\\Icinga\\Module\\' . ucfirst($module) . '\\Filter\\Registry';
$factory = strtolower($domain) . 'Filter';
$this->registry->addDomain($class::$factory());
} }
/**
* Parse the given query text and returns the json as expected by the semantic search box
*
* @param String $text The query to parse
* @return array The result structure to be returned in json format
*/
private function parse($text) private function parse($text)
{ {
try { try {
return $this->registry->getProposalsForQuery($text); $view = HostStatus::fromRequest($this->getRequest());
$urlParser = new UrlViewFilter($view);
$queryTree = $this->registry->createQueryTreeForFilter($text);
return array(
'state' => 'success',
'proposals' => $this->registry->getProposalsForQuery($text),
'urlParam' => $urlParser->fromTree($queryTree)
);
} catch (\Exception $exc) { } catch (\Exception $exc) {
Logger::error($exc); Logger::error($exc);
$this->getResponse()->setHttpResponseCode(500);
return array(
'state' => 'error',
'message' => 'Search service is currently not available'
);
} }
} }
} }
// @codingStandardsIgnoreEnd

View File

@ -1,11 +0,0 @@
All critical hosts starting with 'MySql'
All services with status warning that have been checked in the last two days
Services with open Problems and with critical hosts
with services that are not ok
[(SUBJECT)] [(SPECIFIED)] [(OP)] [FILTER] [(ADDITIONAL)]

View File

@ -26,7 +26,6 @@
*/ */
// {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Filter; namespace Icinga\Filter;
use Icinga\Filter\Query\Node; use Icinga\Filter\Query\Node;

View File

@ -28,7 +28,6 @@
namespace Icinga\Filter; namespace Icinga\Filter;
use Icinga\Filter\Query\Tree; use Icinga\Filter\Query\Tree;
use Icinga\Filter\Query\Node; use Icinga\Filter\Query\Node;
@ -38,7 +37,8 @@ use Icinga\Filter\Query\Node;
* This class handles the top level parsing of queries, i.e. * This class handles the top level parsing of queries, i.e.
* - Splitting queries at conjunctions and parsing them part by part * - Splitting queries at conjunctions and parsing them part by part
* - Delegating the query parts to specific filter domains handling this filters * - Delegating the query parts to specific filter domains handling this filters
* - Building a query tree that allows to convert a filter representation into others (url to string, string to url, sql..) * - Building a query tree that allows to convert a filter representation into others
* (url to string, string to url, sql..)
* *
* Filters are split in Filter Domains, Attributes and Types: * Filters are split in Filter Domains, Attributes and Types:
* *

View File

@ -26,7 +26,6 @@
*/ */
// {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Filter; namespace Icinga\Filter;
use Icinga\Filter\Query\Node; use Icinga\Filter\Query\Node;
@ -134,7 +133,8 @@ class FilterAttribute extends QueryProposer
* *
* @return bool True when this query contains an attribute mapped by this filter * @return bool True when this query contains an attribute mapped by this filter
*/ */
public function queryHasSupportedAttribute($query) { public function queryHasSupportedAttribute($query)
{
return $this->getMatchingAttribute($query) !== null; return $this->getMatchingAttribute($query) !== null;
} }
@ -230,6 +230,4 @@ class FilterAttribute extends QueryProposer
{ {
return new FilterAttribute($type); return new FilterAttribute($type);
} }
} }

View File

@ -27,12 +27,14 @@
// {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Data; namespace Icinga\Filter;
use Icinga\Filter\Query\Tree;
interface Filterable interface Filterable
{ {
public function isValidFilterTarget($targetOrColumn); public function isValidFilterTarget($field);
public function resolveFilterTarget($targetOrColumn); public function getMappedField($field);
public function applyFilter(Tree $filter);
} }

View File

@ -26,7 +26,6 @@
*/ */
// {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Filter\Query; namespace Icinga\Filter\Query;
/** /**
@ -102,7 +101,8 @@ class Node
* Factory method for creating operator nodes * Factory method for creating operator nodes
* *
* @param String $operator The operator to use * @param String $operator The operator to use
* @param String $left The left side of the node, i.e. target (mostly attribute) to query for with this node * @param String $left The left side of the node, i.e. target (mostly attribute)
* to query for with this node
* @param String $right The right side of the node, i.e. the value to use for querying * @param String $right The right side of the node, i.e. the value to use for querying
* *
* @return Node An operator Node instance * @return Node An operator Node instance

View File

@ -26,9 +26,10 @@
*/ */
// {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Filter\Query; namespace Icinga\Filter\Query;
use Icinga\Filter\Filterable;
/** /**
* A binary tree representing queries in an interchangeable way * A binary tree representing queries in an interchangeable way
* *
@ -168,4 +169,215 @@ class Tree
$this->insertOrNode($node, $currentNode->right); $this->insertOrNode($node, $currentNode->right);
} }
} }
/**
* Return a copy of this tree that only contains filters that can be applied for the given Filterable
*
* @param Filterable $filter The Filterable to test element nodes agains
* @return Tree A copy of this tree that only contains nodes for the given filter
*/
public function getCopyForFilterable(Filterable $filter)
{
$copy = $this->createCopy();
if (!$this->root) {
return $copy;
}
$copy->root = $this->removeInvalidFilter($copy->root, $filter);
return $copy;
}
/**
* Remove all tree nodes that are not applicable ot the given Filterable
*
* @param Node $node The root node to use
* @param Filterable $filter The Filterable to test nodes against
* @return Node The normalized tree node
*/
public function removeInvalidFilter($node, Filterable $filter)
{
if ($node === null) {
return $node;
}
if ($node->type === Node::TYPE_OPERATOR) {
if (!$filter->isValidFilterTarget($node->left)) {
return null;
} else {
return $node;
}
}
$node->left = $this->removeInvalidFilter($node->left, $filter);
$node->right = $this->removeInvalidFilter($node->right, $filter);
if ($node->left && $node->right) {
return $node;
} elseif ($node->left) {
return $node->left;
} elseif ($node->right) {
return $node->right;
}
return null;
}
/**
* Normalize this tree and fix incomplete nodes
*
* @param Node $node The root node to normalize
* @return Node The normalized root node
*/
public static function normalizeTree($node)
{
if ($node->type === Node::TYPE_OPERATOR) {
return $node;
}
if ($node === null) {
return null;
}
if ($node->left && $node->right) {
$node->left = self::normalizeTree($node->left);
$node->right = self::normalizeTree($node->right);
return $node;
} elseif ($node->left) {
return $node->left;
} elseif ($node->right) {
return $node->right;
}
}
/**
* Return an array of all attributes in this tree
*
* @param Node $ctx The root node to use instead of the tree root
* @return array An array of attribute names
*/
public function getAttributes($ctx = null)
{
$result = array();
$ctx = $ctx ? $ctx : $this->root;
if ($ctx == null) {
return $result;
}
if ($ctx->type === Node::TYPE_OPERATOR) {
$result[] = $ctx->left;
} else {
$result = $result + $this->getAttributes($ctx->left) + $this->getAttributes($ctx->right);
}
return $result;
}
/**
* Create a copy of this tree without the given node
*
* @param Node $node The node to remove
* @return Tree A copy of the given tree
*/
public function withoutNode(Node $node)
{
$tree = $this->createCopy();
$toRemove = $tree->findNode($node);
if ($toRemove !== null) {
if ($toRemove === $tree->root) {
$tree->root = null;
return $tree;
}
if ($toRemove->parent->left === $toRemove) {
$toRemove->parent->left = null;
} else {
$toRemove->parent->right = null;
}
}
$tree->root = $tree->normalizeTree($tree->root);
return $tree;
}
/**
* Create an independent copy of this tree
*
* @return Tree A copy of this tree
*/
public function createCopy()
{
$tree = new Tree();
if ($this->root === null) {
return $tree;
}
$this->copyBranch($this->root, $tree);
return $tree;
}
/**
* Copy the given node or branch into the given tree
*
* @param Node $node The node to copy
* @param Tree $tree The tree to insert the copied node and it's subnodes to
*/
private function copyBranch(Node $node, Tree &$tree)
{
if ($node->type === Node::TYPE_OPERATOR) {
$copy = Node::createOperatorNode($node->operator, $node->left, $node->right);
$copy->context = $node->context;
$tree->insert($copy);
} else {
if ($node->left) {
$this->copyBranch($node->left, $tree);
}
$tree->insert($node->type === Node::TYPE_OR ? Node::createOrNode() : Node::createAndNode());
if ($node->right) {
$this->copyBranch($node->right, $tree);
}
}
}
/**
* Look for a given node in the tree and return it if exists
*
* @param Node $node The node to look for
* @param Node $ctx The node to use as the root of the tree
*
* @return Node The node that matches $node in the tree or null
*/
public function findNode(Node $node, $ctx = null)
{
$ctx = $ctx ? $ctx : $this->root;
if ($ctx === null) {
return null;
}
if ($ctx->type === Node::TYPE_OPERATOR) {
if ($ctx->left == $node->left && $ctx->right == $node->right && $ctx->operator == $node->operator) {
return $ctx;
}
return null;
} else {
$result = $this->findNode($node, $ctx->left);
if ($result === null) {
$result = $this->findNode($node, $ctx->right);
}
return $result;
}
}
/**
* Return true if A node with the given attribute on the left side exists
*
* @param String $name The attribute to test for existence
* @param Node $ctx The current root node
*
* @return bool True if a node contains $name on the left side, otherwise false
*/
public function hasNodeWithAttribute($name, $ctx = null)
{
$ctx = $ctx ? $ctx : $this->root;
if ($ctx === null) {
return false;
}
if ($ctx->type === Node::TYPE_OPERATOR) {
return $ctx->left === $name;
} else {
return $this->hasNodeWithAttribute($name, $ctx->left) || $this->hasNodeWithAttribute($name, $ctx->right);
}
}
} }

View File

@ -26,7 +26,6 @@
*/ */
// {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Filter; namespace Icinga\Filter;
/** /**
@ -61,5 +60,4 @@ abstract class QueryProposer
* @return array An array containing 0..* proposal text tokens * @return array An array containing 0..* proposal text tokens
*/ */
abstract public function getProposalsForQuery($query); abstract public function getProposalsForQuery($query);
} }

View File

@ -232,5 +232,4 @@ class BooleanFilter extends FilterType
} }
return $node; return $node;
} }
} }

View File

@ -73,7 +73,7 @@ abstract class FilterType extends QueryProposer
* *
* @return bool True when $string starts with $substring * @return bool True when $string starts with $substring
*/ */
static public function startsWith($string, $substring) public static function startsWith($string, $substring)
{ {
return stripos($string, $substring) === 0; return stripos($string, $substring) === 0;
} }

View File

@ -26,10 +26,8 @@
*/ */
// {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Filter\Type; namespace Icinga\Filter\Type;
use Icinga\Filter\Query\Node; use Icinga\Filter\Query\Node;
class TextFilter extends FilterType class TextFilter extends FilterType
@ -166,10 +164,10 @@ class TextFilter extends FilterType
} }
switch (strtolower($operator)) { switch (strtolower($operator)) {
case 'starts with': case 'ends with':
$value = '*' . $value; $value = '*' . $value;
break; break;
case 'ends with': case 'starts with':
$value = $value . '*'; $value = $value . '*';
break; break;
case 'matches': case 'matches':

View File

@ -27,6 +27,7 @@
// {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Filter\Type; namespace Icinga\Filter\Type;
use Icinga\Filter\Query\Node; use Icinga\Filter\Query\Node;
/** /**
@ -104,8 +105,8 @@ class TimeRangeSpecifier extends FilterType
* Return a two element array with the operator and the timestring parsed from the given query part * Return a two element array with the operator and the timestring parsed from the given query part
* *
* @param String $query The query to extract the operator and time value from * @param String $query The query to extract the operator and time value from
* @return array An array containing the operator as the first and the string for strotime as the second * @return array An array containing the operator as the first and the string for
* value or (null,null) if the query is invalid * strotime as the second value or (null,null) if the query is invalid
*/ */
private function getOperatorAndTimeStringFromQuery($query) private function getOperatorAndTimeStringFromQuery($query)
{ {

View File

@ -26,43 +26,85 @@
*/ */
// {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Web\Widget; namespace Icinga\Web\Widget;
use Icinga\Filter\Query\Tree; use Icinga\Filter\Query\Tree;
use Icinga\Filter\Query\Node; use Icinga\Filter\Query\Node;
use Icinga\Module\Monitoring\Filter\UrlViewFilter;
use Icinga\Web\Url;
use Zend_View_Abstract; use Zend_View_Abstract;
/**
* A renderer for filter badges that allow to disable specific filters
*/
class FilterBadgeRenderer implements Widget class FilterBadgeRenderer implements Widget
{ {
private $tree; private $tree;
/**
* @var Url
*/
private $baseUrl;
private $conjunctionCellar = ''; private $conjunctionCellar = '';
private $urlFilter;
/**
* Create a new badge renderer for this tree
*
* @param Tree $tree
*/
public function __construct(Tree $tree) public function __construct(Tree $tree)
{ {
$this->tree = $tree; $this->tree = $tree;
} }
/**
* Create a removable badge from a query tree node
*
* @param Node $node The node to create the badge for
* @return string The html for the badge
*/
private function nodeToBadge(Node $node) private function nodeToBadge(Node $node)
{ {
$basePath = $this->baseUrl->getAbsoluteUrl();
$allParams = $this->baseUrl->getParams();
if ($node->type === Node::TYPE_OPERATOR) { if ($node->type === Node::TYPE_OPERATOR) {
return ' <a class="btn btn-default btn-xs">'
$newTree = $this->tree->withoutNode($node);
$url = $this->urlFilter->fromTree($newTree);
$url = $basePath . (empty($allParams) ? '?' : '&') . $url;
return ' <a class="btn btn-default btn-xs" href="' . $url . '">'
. $this->conjunctionCellar . ' ' . $this->conjunctionCellar . ' '
. ucfirst($node->left) . ' ' . ucfirst($node->left) . ' '
. $node->operator . ' ' . $node->operator . ' '
. $node->right . '</a>'; . $node->right . '</a>';
} }
$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);
return $result; return $result;
} }
/**
* Initialize $this->baseUrl with an Url instance containing all non-filter parameter
*/
private function buildBaseUrl()
{
$baseUrl = Url::fromRequest();
foreach ($baseUrl->getParams() as $key => $param) {
$translated = preg_replace('/[^0-9A-Za-z_]{1,2}$/', '', $key);
if ($this->tree->hasNodeWithAttribute($translated) === true) {
$baseUrl->removeKey($key);
}
}
$this->baseUrl = $baseUrl;
}
/** /**
* Renders this widget via the given view and returns the * Renders this widget via the given view and returns the
@ -73,9 +115,11 @@ class FilterBadgeRenderer implements Widget
*/ */
public function render(Zend_View_Abstract $view) public function render(Zend_View_Abstract $view)
{ {
$this->urlFilter = new UrlViewFilter();
if ($this->tree->root == null) { if ($this->tree->root == null) {
return ''; return '';
} }
$this->buildBaseUrl();
return $this->nodeToBadge($this->tree->root); return $this->nodeToBadge($this->tree->root);
} }
} }

View File

@ -0,0 +1,119 @@
<?php
// {{{ICINGA_LICENSE_HEADER}}}
/**
* This file is part of Icinga 2 Web.
*
* Icinga 2 Web - Head for multiple monitoring backends.
* Copyright (C) 2013 Icinga Development Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* @copyright 2013 Icinga Development Team <info@icinga.org>
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2
* @author Icinga Development Team <info@icinga.org>
*/
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Web\Widget;
use Zend_View_Abstract;
use Icinga\Web\Form;
use Icinga\Web\Url;
use Icinga\Filter\Query\Tree;
/**
* Widget that renders a filter input box together with an FilterBadgeRenderer widget
*/
class FilterBox implements Widget
{
/**
* An optional initial filter to use
*
* @var \Icinga\Filter\Query\Tree
*/
private $initialFilter;
/**
* The domain of the filter, set in the data-icinga-filter-domain attribute
* @var string
*/
private $domain;
/**
* The module of the filter, set in the data-icinga-filter-module attribute
* @var string
*/
private $module;
/**
* The template used for rendering the form and badges
* @var string
*/
private static $TPL = <<<'EOT'
<div class="row">
<div class="col-md-12">{{FORM}}</div>
<div class="col-md-12">{{BADGES}}</div>
</div>
EOT;
/**
* Create a new FilterBox widget
*
* @param Tree $initialFilter The tree to use for initial population
* @param String $domain The filter domain
* @param String $module The filter module
*/
public function __construct(Tree $initialFilter, $domain, $module)
{
$this->initialFilter = $initialFilter;
$this->domain = $domain;
$this->module = $module;
}
/**
* Render this widget
*
* @param Zend_View_Abstract $view The view to use for rendering the widget
* @return string The HTML of the widget as a string
*/
public function render(Zend_View_Abstract $view)
{
$form = new Form();
$form->setAttrib('class', 'form-inline');
$form->setMethod('GET');
$form->setAction(Url::fromPath('/filter'));
$form->setTokenDisabled();
$form->addElement(
'text',
'filter',
array(
'label' => 'Filter Results',
'name' => 'filter',
'data-icinga-component' => 'app/semanticsearch',
'data-icinga-filter-domain' => $this->domain,
'data-icinga-filter-module' => $this->module
)
);
$form->removeAttrib('data-icinga-component');
$form->setIgnoreChangeDiscarding(true);
$badges = new FilterBadgeRenderer($this->initialFilter);
$html = str_replace('{{FORM}}', $form->render($view), self::$TPL);
return str_replace('{{BADGES}}', $badges->render($view), $html);
}
}

View File

@ -38,16 +38,21 @@ use Icinga\Web\Widget\Tabextension\OutputFormat;
use Icinga\Web\Widget\Tabs; use Icinga\Web\Widget\Tabs;
use Icinga\Module\Monitoring\Backend; use Icinga\Module\Monitoring\Backend;
use Icinga\Web\Widget\SortBox; use Icinga\Web\Widget\SortBox;
use Icinga\Web\Widget\FilterBox;
use Icinga\Application\Config as IcingaConfig; use Icinga\Application\Config as IcingaConfig;
use Icinga\Module\Monitoring\DataView\DataView;
use Icinga\Module\Monitoring\DataView\Notification as NotificationView; use Icinga\Module\Monitoring\DataView\Notification as NotificationView;
use Icinga\Module\Monitoring\DataView\Downtime as DowntimeView; use Icinga\Module\Monitoring\DataView\Downtime as DowntimeView;
use Icinga\Module\Monitoring\DataView\Contact as ContactView; use Icinga\Module\Monitoring\DataView\Contact as ContactView;
use Icinga\Module\Monitoring\DataView\Contactgroup as ContactgroupView; use Icinga\Module\Monitoring\DataView\Contactgroup as ContactgroupView;
use Icinga\Module\Monitoring\DataView\HostAndServiceStatus as HostAndServiceStatusView; use Icinga\Module\Monitoring\DataView\HostStatus as HostStatusView;
use Icinga\Module\Monitoring\DataView\ServiceStatus as ServiceStatusView;
use Icinga\Module\Monitoring\DataView\Comment as CommentView; use Icinga\Module\Monitoring\DataView\Comment as CommentView;
use Icinga\Module\Monitoring\DataView\Groupsummary as GroupsummaryView; use Icinga\Module\Monitoring\DataView\Groupsummary as GroupsummaryView;
use Icinga\Module\Monitoring\DataView\EventHistory as EventHistoryView; use Icinga\Module\Monitoring\DataView\EventHistory as EventHistoryView;
use Icinga\Module\Monitoring\Filter\UrlViewFilter;
use Icinga\Filter\Filterable;
class Monitoring_ListController extends MonitoringController class Monitoring_ListController extends MonitoringController
{ {
@ -96,8 +101,9 @@ class Monitoring_ListController extends MonitoringController
*/ */
public function hostsAction() public function hostsAction()
{ {
$this->compactView = 'hosts-compact'; $this->compactView = 'hosts-compact';
$query = HostAndServiceStatusView::fromRequest( $dataview = HostStatusView::fromRequest(
$this->_request, $this->_request,
array( array(
'host_icon_image', 'host_icon_image',
@ -123,8 +129,9 @@ class Monitoring_ListController extends MonitoringController
'host_current_check_attempt', 'host_current_check_attempt',
'host_max_check_attempts' 'host_max_check_attempts'
) )
)->getQuery(); );
$this->view->hosts = $query->paginate(); $query = $dataview->getQuery();
$this->setupFilterControl($dataview);
$this->setupSortControl(array( $this->setupSortControl(array(
'host_last_check' => 'Last Host Check', 'host_last_check' => 'Last Host Check',
'host_severity' => 'Host Severity', 'host_severity' => 'Host Severity',
@ -134,6 +141,8 @@ class Monitoring_ListController extends MonitoringController
'host_state' => 'Hard State' 'host_state' => 'Hard State'
)); ));
$this->handleFormatRequest($query); $this->handleFormatRequest($query);
$this->view->hosts = $query->paginate();
} }
/** /**
@ -390,7 +399,8 @@ class Monitoring_ListController extends MonitoringController
$this->_helper->viewRenderer($this->compactView); $this->_helper->viewRenderer($this->compactView);
} }
if ($this->_getParam('format') === 'sql'
if ($this->getParam('format') === 'sql'
&& IcingaConfig::app()->global->get('environment', 'production') === 'development') { && IcingaConfig::app()->global->get('environment', 'production') === 'development') {
echo '<pre>' echo '<pre>'
. htmlspecialchars(wordwrap($query->dump())) . htmlspecialchars(wordwrap($query->dump()))
@ -426,6 +436,17 @@ class Monitoring_ListController extends MonitoringController
$this->view->sortControl->applyRequest($this->getRequest()); $this->view->sortControl->applyRequest($this->getRequest());
} }
private function setupFilterControl(Filterable $dataview)
{
$parser = new UrlViewFilter($dataview);
$this->view->filterBox = new FilterBox(
$parser->parseUrl(),
'host',
'monitoring'
);
}
/** /**
* Return all tabs for this controller * Return all tabs for this controller
* *

View File

@ -5,8 +5,19 @@ $viewHelper = $this->getHelper('MonitoringState');
<?= $this->tabs->render($this); ?> <?= $this->tabs->render($this); ?>
<h1>Hosts Status</h1> <h1>Hosts Status</h1>
<div data-icinga-component="app/mainDetailGrid"> <div data-icinga-component="app/mainDetailGrid">
<div class="container">
<div class="row">
<div class="col-md-5">
<?= $this->filterBox->render($this); ?>
</div>
<div class="col-md-7">
<?= $this->sortControl->render($this); ?> <?= $this->sortControl->render($this); ?>
</div>
</div>
<div class="row">
<?= $this->paginationControl($hosts, null, null, array('preserve' => $this->preserve)); ?> <?= $this->paginationControl($hosts, null, null, array('preserve' => $this->preserve)); ?>
</div>
</div>
<table class="table table-condensed"> <table class="table table-condensed">
<tbody> <tbody>

View File

@ -1,23 +1,50 @@
<?php <?php
// {{{ICINGA_LICENSE_HEADER}}}
/**
* This file is part of Icinga 2 Web.
*
* Icinga 2 Web - Head for multiple monitoring backends.
* Copyright (C) 2013 Icinga Development Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* @copyright 2013 Icinga Development Team <info@icinga.org>
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2
* @author Icinga Development Team <info@icinga.org>
*/
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Module\Monitoring\Backend\Ido\Query; namespace Icinga\Module\Monitoring\Backend\Ido\Query;
use Icinga\Data\Db\Query; use Icinga\Data\Db\Query;
use Icinga\Application\Benchmark; use Icinga\Application\Benchmark;
use Icinga\Exception\ProgrammingError; use Icinga\Exception\ProgrammingError;
use Icinga\Filter\Query\Tree;
use Icinga\Filter\Filterable;
use Icinga\Module\Monitoring\Filter\Backend\IdoQueryConverter;
use Icinga\Module\Monitoring\Filter\UrlViewFilter;
abstract class AbstractQuery extends Query abstract class AbstractQuery extends Query implements Filterable
{ {
protected $prefix; protected $prefix;
protected $idxAliasColumn; protected $idxAliasColumn;
protected $idxAliasTable; protected $idxAliasTable;
protected $columnMap = array(); protected $columnMap = array();
protected $query; protected $query;
protected $customVars = array(); protected $customVars = array();
protected $joinedVirtualTables = array(); protected $joinedVirtualTables = array();
protected $object_id = 'object_id'; protected $object_id = 'object_id';
protected $host_id = 'host_id'; protected $host_id = 'host_id';
protected $hostgroup_id = 'hostgroup_id'; protected $hostgroup_id = 'hostgroup_id';
@ -25,16 +52,62 @@ abstract class AbstractQuery extends Query
protected $servicegroup_id = 'servicegroup_id'; protected $servicegroup_id = 'servicegroup_id';
protected $contact_id = 'contact_id'; protected $contact_id = 'contact_id';
protected $contactgroup_id = 'contactgroup_id'; protected $contactgroup_id = 'contactgroup_id';
protected $aggregateColumnIdx = array(); protected $aggregateColumnIdx = array();
protected $allowCustomVars = false; protected $allowCustomVars = false;
protected function isAggregateColumn($column) public function isAggregateColumn($column)
{ {
return array_key_exists($column, $this->aggregateColumnIdx); return array_key_exists($column, $this->aggregateColumnIdx);
} }
public function order($col, $dir = null)
{
$this->requireColumn($col);
if ($this->isCustomvar($col)) {
// TODO: Doesn't work right now. Does it?
$col = $this->getCustomvarColumnName($col);
} elseif ($this->hasAliasName($col)) {
$col = $this->aliasToColumnName($col);
} else {
throw new \InvalidArgumentException('Can\'t order by column '.$col);
}
$this->order_columns[] = array($col, $dir);
return $this;
}
public function applyFilter(Tree $filter)
{
foreach ($filter->getAttributes() as $target) {
$this->requireColumn($target);
}
$converter = new IdoQueryConverter($this);
$converter->treeToSql($filter, $this->baseQuery);
}
public function isValidFilterTarget($field)
{
return $this->getMappedField($field) !== null;
}
public function getMappedField($field)
{
foreach ($this->columnMap as $columnSource => $columnSet) {
if (isset($columnSet[$field])) {
return $columnSet[$field];
}
}
return null;
}
public function isTimestamp($field)
{
$mapped = $this->getMappedField($field);
if ($mapped === null) {
return false;
}
return stripos($mapped, 'UNIX_TIMESTAMP') !== false;
}
protected function init() protected function init()
{ {
parent::init(); parent::init();
@ -65,11 +138,136 @@ abstract class AbstractQuery extends Query
$this->prepareAliasIndexes(); $this->prepareAliasIndexes();
} }
protected function joinBaseTables()
{
reset($this->columnMap);
$table = key($this->columnMap);
$this->baseQuery = $this->db->select()->from(
array($table => $this->prefix . $table),
array()
);
$this->joinedVirtualTables = array($table => true);
}
protected function prepareAliasIndexes()
{
foreach ($this->columnMap as $tbl => & $cols) {
foreach ($cols as $alias => $col) {
$this->idxAliasTable[$alias] = $tbl;
$this->idxAliasColumn[$alias] = preg_replace('~\n\s*~', ' ', $col);
}
}
}
protected function beforeCreatingCountQuery()
{
}
protected function beforeCreatingSelectQuery()
{
$this->setRealColumns();
$classParts = explode('\\', get_class($this));
Benchmark::measure(sprintf('%s ready to run', array_pop($classParts)));
}
public function setRealColumns()
{
$columns = $this->columns;
$this->columns = array();
if (empty($columns)) {
$columns = $this->getDefaultColumns();
}
foreach ($columns as $alias => $col) {
$this->requireColumn($col);
if ($this->isCustomvar($col)) {
$name = $this->getCustomvarColumnName($col);
} else {
$name = $this->aliasToColumnName($col);
}
if (is_int($alias)) {
$alias = $col;
}
$this->columns[$alias] = preg_replace('|\n|', ' ', $name);
}
return $this;
}
protected function getDefaultColumns()
{
reset($this->columnMap);
$table = key($this->columnMap);
return array_keys($this->columnMap[$table]);
}
protected function requireColumn($alias)
{
if ($this->hasAliasName($alias)) {
$this->requireVirtualTable($this->aliasToTableName($alias));
} elseif ($this->isCustomVar($alias)) {
$this->requireCustomvar($alias);
} else {
throw new ProgrammingError(sprintf('Got invalid column: %s', $alias));
}
return $this;
}
protected function hasAliasName($alias)
{
return array_key_exists($alias, $this->idxAliasColumn);
}
protected function requireVirtualTable($name)
{
if ($this->hasJoinedVirtualTable($name)) {
return $this;
}
return $this->joinVirtualTable($name);
}
protected function joinVirtualTable($table)
{
$func = 'join' . ucfirst($table);
if (method_exists($this, $func)) {
$this->$func();
} else {
throw new ProgrammingError(
sprintf(
'Cannot join "%s", no such table found',
$table
)
);
}
$this->joinedVirtualTables[$table] = true;
return $this;
}
protected function aliasToTableName($alias)
{
return $this->idxAliasTable[$alias];
}
protected function isCustomVar($alias) protected function isCustomVar($alias)
{ {
return $this->allowCustomVars && $alias[0] === '_'; return $this->allowCustomVars && $alias[0] === '_';
} }
protected function requireCustomvar($customvar)
{
if (! $this->hasCustomvar($customvar)) {
$this->joinCustomvar($customvar);
}
return $this;
}
protected function hasCustomvar($customvar)
{
return array_key_exists($customvar, $this->customVars);
}
protected function joinCustomvar($customvar) protected function joinCustomvar($customvar)
{ {
// TODO: This is not generic enough yet // TODO: This is not generic enough yet
@ -103,200 +301,6 @@ abstract class AbstractQuery extends Query
return $this; return $this;
} }
protected function prepareAliasIndexes()
{
foreach ($this->columnMap as $tbl => & $cols) {
foreach ($cols as $alias => $col) {
$this->idxAliasTable[$alias] = $tbl;
$this->idxAliasColumn[$alias] = preg_replace('~\n\s*~', ' ', $col);
}
}
}
protected function getDefaultColumns()
{
reset($this->columnMap);
$table = key($this->columnMap);
return array_keys($this->columnMap[$table]);
}
protected function joinBaseTables()
{
reset($this->columnMap);
$table = key($this->columnMap);
$this->baseQuery = $this->db->select()->from(
array($table => $this->prefix . $table),
array()
);
$this->joinedVirtualTables = array($table => true);
}
protected function beforeCreatingCountQuery()
{
$this->applyAllFilters();
}
protected function beforeCreatingSelectQuery()
{
$this->setRealColumns();
$classParts = explode('\\', get_class($this));
Benchmark::measure(sprintf('%s ready to run', array_pop($classParts)));
}
protected function applyAllFilters()
{
$filters = array();
foreach ($this->filters as $f) {
$alias = $f[0];
$value = $f[1];
$this->requireColumn($alias);
if ($this->isCustomvar($alias)) {
$col = $this->getCustomvarColumnName($alias);
} elseif ($this->hasAliasName($alias)) {
$col = $this->aliasToColumnName($alias);
} else {
throw new ProgrammingError(
'If you finished here, code has been messed up'
);
}
$func = 'filter' . ucfirst($alias);
if (method_exists($this, $func)) {
$this->$func($value);
return;
}
if ($this->isAggregateColumn($alias)) {
$this->baseQuery->having($this->prepareFilterStringForColumn($col, $value));
} else {
$this->baseQuery->where($this->prepareFilterStringForColumn($col, $value));
}
}
}
public function order($col, $dir = null)
{
$this->requireColumn($col);
if ($this->isCustomvar($col)) {
// TODO: Doesn't work right now. Does it?
$col = $this->getCustomvarColumnName($col);
} elseif ($this->hasAliasName($col)) {
$col = $this->aliasToColumnName($col);
} else {
throw new \InvalidArgumentException('Can\'t order by column '.$col);
}
$this->order_columns[] = array($col, $dir);
return $this;
}
public function setRealColumns()
{
$columns = $this->columns;
$this->columns = array();
if (empty($columns)) {
$colums = $this->getDefaultColumns();
}
foreach ($columns as $alias => $col) {
$this->requireColumn($col);
if ($this->isCustomvar($col)) {
$name = $this->getCustomvarColumnName($col);
} else {
$name = $this->aliasToColumnName($col);
}
if (is_int($alias)) {
$alias = $col;
}
$this->columns[$alias] = preg_replace('|\n|', ' ' , $name);
}
return $this;
}
protected function requireColumn($alias)
{
if ($this->hasAliasName($alias)) {
$this->requireVirtualTable($this->aliasToTableName($alias));
} elseif ($this->isCustomVar($alias)) {
$this->requireCustomvar($alias);
} else {
throw new ProgrammingError(sprintf('Got invalid column: %s', $alias));
}
return $this;
}
protected function hasAliasName($alias)
{
return array_key_exists($alias, $this->idxAliasColumn);
}
public function aliasToColumnName($alias)
{
return $this->idxAliasColumn[$alias];
}
protected function aliasToTableName($alias)
{
return $this->idxAliasTable[$alias];
}
protected function hasJoinedVirtualTable($name)
{
return array_key_exists($name, $this->joinedVirtualTables);
}
protected function requireVirtualTable($name)
{
if ($this->hasJoinedVirtualTable($name)) {
return $this;
}
return $this->joinVirtualTable($name);
}
protected function joinVirtualTable($table)
{
$func = 'join' . ucfirst($table);
if (method_exists($this, $func)) {
$this->$func();
} else {
throw new ProgrammingError(sprintf(
'Cannot join "%s", no such table found',
$table
));
}
$this->joinedVirtualTables[$table] = true;
return $this;
}
protected function requireCustomvar($customvar)
{
if (! $this->hasCustomvar($customvar)) {
$this->joinCustomvar($customvar);
}
return $this;
}
protected function hasCustomvar($customvar)
{
return array_key_exists($customvar, $this->customVars);
}
protected function getCustomvarColumnName($customvar)
{
return $this->customVars[$customvar] . '.varvalue';
}
protected function createSubQuery($queryName, $columns = array())
{
$class = '\\'
. substr(__CLASS__, 0, strrpos(__CLASS__, '\\') + 1)
. ucfirst($queryName) . 'Query';
$query = new $class($this->ds, $columns);
return $query;
}
protected function customvarNameToTypeName($customvar) protected function customvarNameToTypeName($customvar)
{ {
// TODO: Improve this: // TODO: Improve this:
@ -311,106 +315,27 @@ abstract class AbstractQuery extends Query
return array($m[1], $m[2]); return array($m[1], $m[2]);
} }
protected function prepareFilterStringForColumn($column, $value) protected function hasJoinedVirtualTable($name)
{ {
$filter = ''; return array_key_exists($name, $this->joinedVirtualTables);
$filters = array();
$or = array();
$and = array();
if (
! is_array($value) &&
(strpos($value, ',') !== false || strpos($value, '|') !== false)
) {
$value = preg_split('~[,|]~', $value, -1, PREG_SPLIT_NO_EMPTY);
}
if (! is_array($value)) {
$value = array($value);
} }
// Go through all given values protected function getCustomvarColumnName($customvar)
foreach ($value as $val) {
if ($val === '') {
// TODO: REALLY??
continue;
}
$not = false;
$force = false;
$op = '=';
$wildcard = false;
if ($val[0] === '-' || $val[0] === '!') {
// Value starting with minus or !: negation
$val = substr($val, 1);
$not = true;
}
if ($val[0] === '+') {
// Value starting with +: enforces AND
// TODO: depends on correct URL handling, not given in all
// ZF versions.
$val = substr($val, 1);
$force = true;
}
if ($val[0] === '<' || $val[0] === '>') {
$op = $val[0];
$val = substr($val, 1);
}
if (strpos($val, '*') !== false) {
$wildcard = true;
$val = str_replace('*', '%', $val);
}
$operator = null;
switch ($op) {
case '=':
if ($not) {
$operator = $wildcard ? 'NOT LIKE' : '!=';
} else {
$operator = $wildcard ? 'LIKE' : '=';
}
break;
case '>':
$operator = $not ? '<=' : '>';
break;
case '<':
$operator = $not ? '>=' : '<';
break;
default:
throw new ProgrammingError("'$op' is not a valid operator");
}
if ($not || $force) {
$and[] = $this->db->quoteInto($column . ' ' . $operator . ' ?', $val);
} else {
$or[] = $this->db->quoteInto($column . ' ' . $operator . ' ?', $val);
}
}
if (! empty($or)) {
$filters[] = implode(' OR ', $or);
}
if (! empty($and)) {
$filters[] = implode(' AND ', $and);
}
if (! empty($filters)) {
$filter = '(' . implode(') AND (', $filters) . ')';
}
return $filter;
}
public function getMappedColumn($name)
{ {
foreach ($this->columnMap as $column => $results) { return $this->customVars[$customvar] . '.varvalue';
if (isset($results[$name])) {
return $results[$name];
}
} }
return null; public function aliasToColumnName($alias)
{
return $this->idxAliasColumn[$alias];
}
protected function createSubQuery($queryName, $columns = array())
{
$class = '\\'
. substr(__CLASS__, 0, strrpos(__CLASS__, '\\') + 1)
. ucfirst($queryName) . 'Query';
$query = new $class($this->ds, $columns);
return $query;
} }
} }

View File

@ -1,11 +1,36 @@
<?php <?php
// {{{ICINGA_LICENSE_HEADER}}}
/**
* This file is part of Icinga 2 Web.
*
* Icinga 2 Web - Head for multiple monitoring backends.
* Copyright (C) 2013 Icinga Development Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* @copyright 2013 Icinga Development Team <info@icinga.org>
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2
* @author Icinga Development Team <info@icinga.org>
*/
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Module\Monitoring\Backend\Ido\Query; namespace Icinga\Module\Monitoring\Backend\Ido\Query;
class HoststatusQuery extends AbstractQuery class HoststatusQuery extends AbstractQuery
{ {
protected $allowCustomVars = true; protected $allowCustomVars = true;
protected $columnMap = array( protected $columnMap = array(
'hosts' => array( 'hosts' => array(
'host' => 'ho.name1 COLLATE latin1_general_ci', 'host' => 'ho.name1 COLLATE latin1_general_ci',
@ -43,7 +68,6 @@ class HoststatusQuery extends AbstractQuery
'host_last_time_unreachable' => 'hs.last_time_unreachable', 'host_last_time_unreachable' => 'hs.last_time_unreachable',
'host_current_check_attempt' => 'hs.current_check_attempt', 'host_current_check_attempt' => 'hs.current_check_attempt',
'host_max_check_attempts' => 'hs.max_check_attempts', 'host_max_check_attempts' => 'hs.max_check_attempts',
'host_severity' => 'CASE WHEN hs.current_state = 0 'host_severity' => 'CASE WHEN hs.current_state = 0
THEN THEN
CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL
@ -103,14 +127,12 @@ class HoststatusQuery extends AbstractQuery
'services_unknown_unhandled' => 'SUM(CASE WHEN ss.current_state = 3 AND (ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0) THEN 1 ELSE 0 END)', 'services_unknown_unhandled' => 'SUM(CASE WHEN ss.current_state = 3 AND (ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0) THEN 1 ELSE 0 END)',
), ),
); );
protected $aggregateColumnIdx = array( protected $aggregateColumnIdx = array(
'services_cnt' => true, 'services_cnt' => true,
'services_problem' => true, 'services_problem' => true,
'services_problem_handled' => true, 'services_problem_handled' => true,
'services_problem_unhandled' => true, 'services_problem_unhandled' => true,
); );
protected $hcgSub; protected $hcgSub;
protected function getDefaultColumns() protected function getDefaultColumns()
@ -184,6 +206,46 @@ class HoststatusQuery extends AbstractQuery
} }
} }
protected function joinServiceHostgroups()
{
$this->baseQuery->join(
array('hgm' => $this->prefix . 'hostgroup_members'),
'hgm.host_object_id = s.host_object_id',
array()
)->join(
array('hg' => $this->prefix . 'hostgroups'),
'hgm.hostgroup_id = hg.' . $this->hostgroup_id,
array()
)->join(
array('hgo' => $this->prefix . 'objects'),
'hgo.' . $this->object_id . ' = hg.hostgroup_object_id'
. ' AND hgo.is_active = 1',
array()
);
return $this;
}
protected function joinHostHostgroups()
{
$this->baseQuery->join(
array('hgm' => $this->prefix . 'hostgroup_members'),
'hgm.host_object_id = h.host_object_id',
array()
)->join(
array('hg' => $this->prefix . 'hostgroups'),
"hgm.hostgroup_id = hg.$this->hostgroup_id",
array()
)->join(
array('hgo' => $this->prefix . 'objects'),
'hgo.' . $this->object_id . ' = hg.hostgroup_object_id'
. ' AND hgo.is_active = 1',
array()
);
return $this;
}
protected function joinContacts() protected function joinContacts()
{ {
$this->hcgcSub = $this->db->select()->distinct()->from( $this->hcgcSub = $this->db->select()->distinct()->from(
@ -212,48 +274,6 @@ class HoststatusQuery extends AbstractQuery
return $this; return $this;
} }
/*
protected function joinContacts()
{
$this->baseQuery->join(
array('hc' => $this->prefix . 'host_contacts'),
'hc.host_id = h.host_id',
array()
)->join(
array('hco' => $this->prefix . 'objects'),
'hco.' . $this->object_id. ' = hc.contact_object_id'
. ' AND hco.is_active = 1',
array()
);
$this->baseQuery->join(
array('hcg' => $this->prefix . 'host_contactgroups'),
'hcg.host_id = h.host_id',
array()
)->join(
array('hcgo' => $this->prefix . 'objects'),
'hcgo.' . $this->object_id. ' = hcg.contactgroup_object_id'
. ' AND hcgo.is_active = 1',
array()
);
$this->baseQuery->join(
array('cgm' => $this->prefix . 'contactgroup_members'),
'cgm.contactgroup_id = cg.contactgroup_id',
array()
)->join(
array('co' => $this->prefix . 'objects'),
'cgm.contact_object_id = co.object_id AND co.is_active = 1',
array()
);
}
return $this;
}
*/
protected function filterContactgroup($value) protected function filterContactgroup($value)
{ {
$this->hcgSub->where( $this->hcgSub->where(
@ -265,6 +285,17 @@ class HoststatusQuery extends AbstractQuery
return $this; return $this;
} }
protected function joinContactgroups()
{
$this->hcgSub = $this->createContactgroupFilterSubselect();
$this->baseQuery->join(
array('hcg' => $this->hcgSub),
'hcg.object_id = ho.object_id',
array()
);
return $this;
}
protected function createContactgroupFilterSubselect() protected function createContactgroupFilterSubselect()
{ {
@ -287,58 +318,6 @@ class HoststatusQuery extends AbstractQuery
)); ));
} }
protected function joinContactgroups()
{
$this->hcgSub = $this->createContactgroupFilterSubselect();
$this->baseQuery->join(
array('hcg' => $this->hcgSub),
'hcg.object_id = ho.object_id',
array()
);
return $this;
}
protected function joinHostHostgroups()
{
$this->baseQuery->join(
array('hgm' => $this->prefix . 'hostgroup_members'),
'hgm.host_object_id = h.host_object_id',
array()
)->join(
array('hg' => $this->prefix . 'hostgroups'),
"hgm.hostgroup_id = hg.$this->hostgroup_id",
array()
)->join(
array('hgo' => $this->prefix . 'objects'),
'hgo.' . $this->object_id. ' = hg.hostgroup_object_id'
. ' AND hgo.is_active = 1',
array()
);
return $this;
}
protected function joinServiceHostgroups()
{
$this->baseQuery->join(
array('hgm' => $this->prefix . 'hostgroup_members'),
'hgm.host_object_id = s.host_object_id',
array()
)->join(
array('hg' => $this->prefix . 'hostgroups'),
'hgm.hostgroup_id = hg.' . $this->hostgroup_id,
array()
)->join(
array('hgo' => $this->prefix . 'objects'),
'hgo.' . $this->object_id. ' = hg.hostgroup_object_id'
. ' AND hgo.is_active = 1',
array()
);
return $this;
}
protected function joinServicegroups() protected function joinServicegroups()
{ {
// TODO: Only hosts with services having such servicegroups // TODO: Only hosts with services having such servicegroups

View File

@ -4,7 +4,7 @@ namespace \Icinga\Module\Monitoring\Backend\Livestatus\Query;
use Icinga\Data\AbstractQuery; use Icinga\Data\AbstractQuery;
class StatusQuery extends AbstractQuery class StatusQuery extends AbstractQuery implements Filterable
{ {
protected $available_columns = array( protected $available_columns = array(
'host_name', 'host_name',

View File

@ -28,17 +28,19 @@
namespace Icinga\Module\Monitoring\Backend\Statusdat\Query; namespace Icinga\Module\Monitoring\Backend\Statusdat\Query;
use \Icinga\Module\Monitoring\Backend\Statusdat\Criteria\Order; use Icinga\Filter\Query\Tree;
use Icinga\Protocol\Statusdat; use Icinga\Protocol\Statusdat;
use Icinga\Exception; use Icinga\Exception;
use Icinga\Data\AbstractQuery; use Icinga\Data\AbstractQuery;
use Icinga\Protocol\Statusdat\View\MonitoringObjectList as MList; use Icinga\Protocol\Statusdat\View\MonitoringObjectList as MList;
use Icinga\Protocol\Statusdat\Query as StatusdatQuery; use Icinga\Protocol\Statusdat\Query as StatusdatQuery;
use Icinga\Filter\Filterable;
/** /**
* Class Query * Class Query
* @package Icinga\Backend\Statusdat * @package Icinga\Backend\Statusdat
*/ */
abstract class Query extends AbstractQuery abstract class Query extends AbstractQuery implements Filterable
{ {
/** /**
* @var null * @var null
@ -284,7 +286,22 @@ abstract class Query extends AbstractQuery
*/ */
public function count() public function count()
{ {
return count($this->baseQuery->getResult()); return count($this->baseQuery->getResult());
} }
public function isValidFilterTarget($field)
{
// TODO: Implement isValidFilterTarget() method.
} }
public function getMappedField($field)
{
// TODO: Implement getMappedField() method.
}
public function applyFilter(Tree $filter)
{
// TODO: Implement applyFilter() method.
}
}

View File

@ -5,14 +5,25 @@
namespace Icinga\Module\Monitoring\DataView; namespace Icinga\Module\Monitoring\DataView;
use Icinga\Data\AbstractQuery; use Icinga\Data\AbstractQuery;
use Icinga\Filter\Filterable;
use Icinga\Filter\Query\Tree;
use Icinga\Module\Monitoring\Backend; use Icinga\Module\Monitoring\Backend;
use Icinga\Module\Monitoring\Filter\UrlViewFilter;
use Icinga\Web\Request; use Icinga\Web\Request;
/** /**
* A read-only view of an underlying Query * A read-only view of an underlying Query
*/ */
abstract class DataView abstract class DataView implements Filterable
{ {
/**
* Sort in ascending order, default
*/
const SORT_ASC = AbstractQuery::SORT_ASC;
/**
* Sort in reverse order
*/
const SORT_DESC = AbstractQuery::SORT_DESC;
/** /**
* The query used to populate the view * The query used to populate the view
* *
@ -20,16 +31,6 @@ abstract class DataView
*/ */
private $query; private $query;
/**
* Sort in ascending order, default
*/
const SORT_ASC = AbstractQuery::SORT_ASC;
/**
* Sort in reverse order
*/
const SORT_DESC = AbstractQuery::SORT_DESC;
/** /**
* Create a new view * Create a new view
* *
@ -38,7 +39,9 @@ abstract class DataView
*/ */
public function __construct(Backend $ds, array $columns = null) public function __construct(Backend $ds, array $columns = null)
{ {
$filter = new UrlViewFilter($this);
$this->query = $ds->select()->from(static::getTableName(), $columns === null ? $this->getColumns() : $columns); $this->query = $ds->select()->from(static::getTableName(), $columns === null ? $this->getColumns() : $columns);
$this->applyFilter($filter->parseUrl());
} }
/** /**
@ -60,16 +63,9 @@ abstract class DataView
*/ */
abstract public function getColumns(); abstract public function getColumns();
/** public function applyFilter(Tree $filter)
* Retrieve default sorting rules for particular columns. These involve sort order and potential additional to sort
*
* @return array
*/
abstract public function getSortRules();
public function getFilterColumns()
{ {
return array(); return $this->query->applyFilter($filter);
} }
/** /**
@ -131,12 +127,12 @@ abstract class DataView
* *
* @param array $filters * @param array $filters
* *
* @see isValidFilterColumn() * @see Filterable::isValidFilterTarget()
*/ */
public function filter(array $filters) public function filter(array $filters)
{ {
foreach ($filters as $column => $filter) { foreach ($filters as $column => $filter) {
if ($this->isValidFilterColumn($column)) { if ($this->isValidFilterTarget($column)) {
$this->query->where($column, $filter); $this->query->where($column, $filter);
} }
} }
@ -150,11 +146,16 @@ abstract class DataView
* *
* @return bool * @return bool
*/ */
public function isValidFilterColumn($column) public function isValidFilterTarget($column)
{ {
return in_array($column, $this->getColumns()) || in_array($column, $this->getFilterColumns()); return in_array($column, $this->getColumns()) || in_array($column, $this->getFilterColumns());
} }
public function getFilterColumns()
{
return array();
}
/** /**
* Sort the rows, according to the specified sort column and order * Sort the rows, according to the specified sort column and order
* *
@ -191,6 +192,18 @@ abstract class DataView
} }
} }
/**
* Retrieve default sorting rules for particular columns. These involve sort order and potential additional to sort
*
* @return array
*/
abstract public function getSortRules();
public function getMappedField($field)
{
return $this->query->getMappedField($field);
}
/** /**
* Return the query which was created in the constructor * Return the query which was created in the constructor
* *
@ -200,4 +213,9 @@ abstract class DataView
{ {
return $this->query; return $this->query;
} }
public function getFilterDomain()
{
return null;
}
} }

View File

@ -0,0 +1,101 @@
<?php
// {{{ICINGA_LICENSE_HEADER}}}
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Module\Monitoring\DataView;
use Icinga\Module\Monitoring\Filter\MonitoringFilter;
class HostStatus extends DataView
{
/**
* Retrieve columns provided by this view
*
* @return array
*/
public function getColumns()
{
return array(
'host_name',
'host_state',
'host_state_type',
'host_last_state_change',
'host_address',
'host_handled',
'host_icon_image',
'host_acknowledged',
'host_output',
'host_long_output',
'host_in_downtime',
'host_is_flapping',
'host_last_check',
'host_next_check',
'host_notifications_enabled',
'host_unhandled_service_count',
'host_action_url',
'host_notes_url',
'host_last_comment',
'host',
'host_display_name',
'host_alias',
'host_ipv4',
'host_severity',
'host_perfdata',
'host_does_active_checks',
'host_accepts_passive_checks',
'host_last_hard_state',
'host_last_hard_state_change',
'host_last_time_up',
'host_last_time_down',
'host_last_time_unreachable'
);
}
public static function getTableName()
{
return 'status';
}
public function getSortRules()
{
return array(
'host_name' => array(
'order' => self::SORT_ASC
),
'host_address' => array(
'columns' => array(
'host_ipv4',
'service_description'
),
'order' => self::SORT_ASC
),
'host_last_state_change' => array(
'order' => self::SORT_ASC
),
'host_severity' => array(
'columns' => array(
'host_severity',
'host_last_state_change',
),
'order' => self::SORT_ASC
)
);
}
public function getFilterColumns()
{
return array('hostgroups', 'servicegroups', 'service_problems');
}
public function isValidFilterTarget($column)
{
if ($column[0] === '_'
&& preg_match('/^_(?:host|service)_/', $column)
) {
return true;
}
return parent::isValidFilterTarget($column);
}
}

View File

@ -4,7 +4,7 @@
namespace Icinga\Module\Monitoring\DataView; namespace Icinga\Module\Monitoring\DataView;
class HostAndServiceStatus extends DataView class ServiceStatus extends DataView
{ {
/** /**
* Retrieve columns provided by this view * Retrieve columns provided by this view
@ -54,7 +54,6 @@ class HostAndServiceStatus extends DataView
'host_display_name', 'host_display_name',
'host_alias', 'host_alias',
'host_ipv4', 'host_ipv4',
// 'host_problems',
'host_severity', 'host_severity',
'host_perfdata', 'host_perfdata',
'host_does_active_checks', 'host_does_active_checks',
@ -65,7 +64,6 @@ class HostAndServiceStatus extends DataView
'host_last_time_down', 'host_last_time_down',
'host_last_time_unreachable', 'host_last_time_unreachable',
'service', 'service',
// 'current_state',
'service_hard_state', 'service_hard_state',
'service_perfdata', 'service_perfdata',
'service_does_active_checks', 'service_does_active_checks',
@ -78,13 +76,11 @@ class HostAndServiceStatus extends DataView
'service_last_time_unknown', 'service_last_time_unknown',
'service_current_check_attempt', 'service_current_check_attempt',
'service_max_check_attempts' 'service_max_check_attempts'
// 'object_type',
// 'problems',
// 'handled',
// 'severity'
); );
} }
public static function getTableName() public static function getTableName()
{ {
return 'status'; return 'status';
@ -121,13 +117,13 @@ class HostAndServiceStatus extends DataView
return array('hostgroups', 'servicegroups', 'service_problems'); return array('hostgroups', 'servicegroups', 'service_problems');
} }
public function isValidFilterColumn($column) public function isValidFilterTarget($column)
{ {
if ($column[0] === '_' if ($column[0] === '_'
&& preg_match('/^_(?:host|service)_/', $column) && preg_match('/^_(?:host|service)_/', $column)
) { ) {
return true; return true;
} }
return parent::isValidFilterColumn($column); return parent::isValidFilterTarget($column);
} }
} }

View File

@ -26,33 +26,50 @@
*/ */
// {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Module\Monitoring\Filter\Backend; namespace Icinga\Module\Monitoring\Filter\Backend;
use Icinga\Data\DatasourceInterface;
use Icinga\Data\Db\Query;
use Icinga\Filter\Query\Tree; use Icinga\Filter\Query\Tree;
use Icinga\Filter\Query\Node; use Icinga\Filter\Query\Node;
use Icinga\Filter\Filterable;
use Icinga\Module\Monitoring\DataView\DataView; use Icinga\Module\Monitoring\DataView\DataView;
use Icinga\Module\Monitoring\Backend\Ido\Query\AbstractQuery;
/**
* Converter class that takes a query tree and creates an SQL Query from it's state
*/
class IdoQueryConverter class IdoQueryConverter
{ {
private $view; /**
* The query class to use as the base for converting
*
* @var AbstractQuery
*/
private $query; private $query;
private $params = array();
public function getParams() /**
* The type of the filter (WHERE or HAVING, depending whether it's an aggregate query)
* @var string
*/
private $type = 'WHERE';
/**
* Create a new converter from this query
*
* @param AbstractQuery $query The query to use for conversion
*/
public function __construct(AbstractQuery $query)
{ {
return $this->params; $this->query = $query;
}
public function __construct(DataView $view, array $initialParams = array())
{
$this->view = $view;
$this->query = $this->view->getQuery();
$this->params = $initialParams;
} }
/**
* Return the SQL equivalent fo the given text operator
*
* @param String $operator The operator from the query node
* @return string The operator for the sql query part
*/
private function getSqlOperator($operator) private function getSqlOperator($operator)
{ {
switch($operator) { switch($operator) {
@ -65,6 +82,12 @@ class IdoQueryConverter
} }
} }
/**
* Convert a Query Tree node to an sql string
*
* @param Node $node The node to convert
* @return string The sql string representing the node's state
*/
private function nodeToSqlQuery(Node $node) private function nodeToSqlQuery(Node $node)
{ {
if ($node->type !== Node::TYPE_OPERATOR) { if ($node->type !== Node::TYPE_OPERATOR) {
@ -74,6 +97,12 @@ class IdoQueryConverter
} }
} }
/**
* Parse an AND or OR node to an sql string
*
* @param Node $node The AND/OR node to parse
* @return string The sql string representing this node
*/
private function parseConjunctionNode(Node $node) private function parseConjunctionNode(Node $node)
{ {
$queryString = ''; $queryString = '';
@ -88,33 +117,78 @@ class IdoQueryConverter
return $queryString; return $queryString;
} }
/**
* Parse an operator node to an sql string
*
* @param Node $node The operator node to parse
* @return string The sql string representing this node
*/
private function parseOperatorNode(Node $node) private function parseOperatorNode(Node $node)
{ {
if (!$this->view->isValidFilterColumn($node->left) && $this->query->getMappedColumn($node->left)) { if (!$this->query->isValidFilterTarget($node->left) && $this->query->getMappedField($node->left)) {
return ''; return '';
} }
$queryString = $this->query->getMappedColumn($node->left); $queryString = $this->query->getMappedField($node->left);
$queryString .= ' ' . (is_integer($node->right) ? $node->operator : $this->getSqlOperator($node->operator)); if ($this->query->isAggregateColumn($node->left)) {
$queryString .= ' ? '; $this->type = 'HAVING';
$this->params[] = $this->getParameterValue($node); }
$queryString .= ' ' . (is_integer($node->right) ? $node->operator : $this->getSqlOperator($node->operator)) . ' ';
$queryString .= $this->getParameterValue($node);
return $queryString; return $queryString;
} }
/**
* Convert a node value to it's sql equivalent
*
* This currently only detects if the node is in the timestring context and calls strtotime if so and it replaces
* '*' with '%'
*
* @param Node $node The node to retrieve the sql string value from
* @return String|int The converted and quoted value
*/
private function getParameterValue(Node $node) { private function getParameterValue(Node $node) {
$value = $node->right;
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;
}
switch($node->context) { switch($node->context) {
case Node::CONTEXT_TIMESTRING: case Node::CONTEXT_TIMESTRING:
return strtotime($node->right); $value = strtotime($value);
default: default:
return $node->right; break;
}
return $this->query->getDatasource()->getConnection()->quote($value);
}
/**
* Apply the given tree to the query, either as where or as having clause
*
* @param Tree $tree The tree representing the filter
* @param \Zend_Db_Select $baseQuery The query to apply the filter on
*/
public function treeToSql(Tree $tree, $baseQuery)
{
if ($tree->root == null) {
return;
}
$sql = $this->nodeToSqlQuery($tree->root);
if ($this->filtersAggregate()) {
$baseQuery->having($sql);
} else {
$baseQuery->where($sql);
} }
} }
public function treeToSql(Tree $tree) /**
* Return true if this is an filter that should be applied after aggregation
*
* @return bool True when having should be used, otherwise false
*/
private function filtersAggregate()
{ {
if ($tree->root == null) { return $this->type === 'HAVING';
return '';
}
return $this->nodeToSqlQuery($tree->root);
} }
} }

View File

@ -26,7 +26,6 @@
*/ */
// {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Module\Monitoring\Filter; namespace Icinga\Module\Monitoring\Filter;
use Icinga\Filter\Domain; use Icinga\Filter\Domain;
@ -41,11 +40,9 @@ use Icinga\Module\Monitoring\Filter\Type\StatusFilter;
* Factory class to create filter for different monitoring objects * Factory class to create filter for different monitoring objects
* *
*/ */
class MonitoringFilter class Registry
{ {
public static function getNextCheckFilterType()
private static function getNextCheckFilterType()
{ {
$type = new TimeRangeSpecifier(); $type = new TimeRangeSpecifier();
$type->setOperator( $type->setOperator(
@ -57,7 +54,7 @@ class MonitoringFilter
return $type; return $type;
} }
private static function getLastCheckFilterType() public static function getLastCheckFilterType()
{ {
$type = new TimeRangeSpecifier(); $type = new TimeRangeSpecifier();
$type->setOperator( $type->setOperator(
@ -84,7 +81,8 @@ class MonitoringFilter
->setHandledAttributes('State', 'Status', 'Current Status') ->setHandledAttributes('State', 'Status', 'Current Status')
->setField('host_state') ->setField('host_state')
)->registerAttribute( )->registerAttribute(
FilterAttribute::create(new BooleanFilter(array( FilterAttribute::create(new BooleanFilter(
array(
'host_is_flapping' => 'Flapping', 'host_is_flapping' => 'Flapping',
'host_problem' => 'In Problem State', 'host_problem' => 'In Problem State',
'host_notifications_enabled' => 'Sending Notifications', 'host_notifications_enabled' => 'Sending Notifications',
@ -92,7 +90,8 @@ class MonitoringFilter
'host_passive_checks_enabled' => 'Accepting Passive Checks', 'host_passive_checks_enabled' => 'Accepting Passive Checks',
'host_handled' => 'Handled', 'host_handled' => 'Handled',
'host_in_downtime' => 'In Downtime', 'host_in_downtime' => 'In Downtime',
))) )
))
)->registerAttribute( )->registerAttribute(
FilterAttribute::create(self::getLastCheckFilterType()) FilterAttribute::create(self::getLastCheckFilterType())
->setHandledAttributes('Last Check', 'Check') ->setHandledAttributes('Last Check', 'Check')
@ -104,5 +103,4 @@ class MonitoringFilter
); );
return $domain; return $domain;
} }
} }

View File

@ -30,11 +30,15 @@
namespace Icinga\Module\Monitoring\Filter; namespace Icinga\Module\Monitoring\Filter;
use Icinga\Filter\Filterable;
use Icinga\Filter\Query\Tree; use Icinga\Filter\Query\Tree;
use Icinga\Filter\Query\Node; use Icinga\Filter\Query\Node;
use Icinga\Web\Url; use Icinga\Web\Url;
use Icinga\Application\Logger; use Icinga\Application\Logger;
/**
* Converter class that allows to create Query Trees from an request query and vice versa
*/
class UrlViewFilter class UrlViewFilter
{ {
const FILTER_TARGET = 'target'; const FILTER_TARGET = 'target';
@ -42,27 +46,54 @@ class UrlViewFilter
const FILTER_VALUE = 'value'; const FILTER_VALUE = 'value';
const FILTER_ERROR = 'error'; const FILTER_ERROR = 'error';
private function evaluateNode(Node $node) /**
* An optional target filterable to use for validation and normalization
*
* @var Filterable
*/
private $target;
/**
* Create a new ViewFilter
*
* @param Filterable $target An optional Filterable to use for validation and normalization
*/
public function __construct(Filterable $target = null)
{ {
switch($node->type) { $this->target = $target;
case Node::TYPE_OPERATOR:
return urlencode($node->left) . $node->operator . urlencode($node->right);
case Node::TYPE_AND:
return $this->evaluateNode($node->left) . '&' . $this->evaluateNode($node->right);
case Node::TYPE_OR:
return $this->evaluateNode($node->left) . '|' . $this->evaluateNode($node->right);
}
} }
/**
* Return an URL filter string for the given query tree
*
* @param Tree $filter The query tree to parse
* @return null|string The string representation of the query
*/
public function fromTree(Tree $filter) public function fromTree(Tree $filter)
{ {
return $this->evaluateNode($filter->root); if ($filter->root === null) {
return '';
}
if ($this->target) {
$filter = $filter->getCopyForFilterable($this->target);
}
return $this->convertNodeToUrlString($filter->root);
} }
public function parseUrl($query = "") /**
* Parse the given given url and return a query tree
*
* @param string $query The query to parse, if not given $_SERVER['QUERY_STRING'] is used
* @return Tree A tree representing the valid parts of the filter
*/
public function parseUrl($query = '')
{ {
if (!isset($_SERVER['QUERY_STRING'])) {
$_SERVER['QUERY_STRING'] = $query;
}
$query = $query ? $query : $_SERVER['QUERY_STRING']; $query = $query ? $query : $_SERVER['QUERY_STRING'];
$tokens = $this->tokenizeQuery($query); $tokens = $this->tokenizeQuery($query);
$tree = new Tree(); $tree = new Tree();
foreach ($tokens as $token) { foreach ($tokens as $token) {
@ -80,18 +111,70 @@ class UrlViewFilter
); );
} }
} }
return $tree; return $tree->getCopyForFilterable($this->target);
} }
/**
* Convert a tree node and it's subnodes to a request string
*
* @param Node $node The node to convert
* @return null|string A string representing the node in the url form or null if it's invalid
* ( or if the Filterable doesn't support the attribute)
*/
private function convertNodeToUrlString(Node $node)
{
$left = null;
$right = null;
if ($node->type === Node::TYPE_OPERATOR) {
if ($this->target && !$this->target->isValidFilterTarget($node->left)) {
return null;
}
return urlencode($node->left) . $node->operator . urlencode($node->right);
}
if ($node->left) {
$left = $this->convertNodeToUrlString($node->left);
}
if ($node->right) {
$right = $this->convertNodeToUrlString($node->right);
}
if ($left && !$right) {
return null;
} elseif ($right && !$left) {
return $this->convertNodeToUrlString($node->right);
} elseif (!$left && !$right) {
return null;
}
$operator = ($node->type === Node::TYPE_AND) ? '&' : '|';
return $left . $operator . $right;
}
/**
* Split the query into seperate tokens that can be parsed seperately
*
* Tokens are associative arrays in the following form
*
* array(
* self::FILTER_TARGET => 'Attribute',
* self::FILTER_OPERATOR => '!=',
* self::FILTER_VALUE => 'Value'
* )
*
* @param String $query The query to tokenize
* @return array An array of tokens
*
* @see self::parseTarget() The tokenize function for target=value expressions
* @see self::parseValue() The tokenize function that only retrieves a value (e.g. target=value|value2)
*/
private function tokenizeQuery($query) private function tokenizeQuery($query)
{ {
$tokens = array(); $tokens = array();
$state = self::FILTER_TARGET; $state = self::FILTER_TARGET;
$query = urldecode($query);
for ($i = 0; $i <= strlen($query); $i++) { for ($i = 0; $i <= strlen($query); $i++) {
switch ($state) { switch ($state) {
case self::FILTER_TARGET: case self::FILTER_TARGET:
list($i, $state) = $this->parseTarget($query, $i, $tokens); list($i, $state) = $this->parseTarget($query, $i, $tokens);
@ -100,7 +183,7 @@ class UrlViewFilter
list($i, $state) = $this->parseValue($query, $i, $tokens); list($i, $state) = $this->parseValue($query, $i, $tokens);
break; break;
case self::FILTER_ERROR: case self::FILTER_ERROR:
list($i, $state) = $this->skip($query, $i, $tokens); list($i, $state) = $this->skip($query, $i);
break; break;
} }
} }
@ -108,6 +191,14 @@ class UrlViewFilter
return $tokens; return $tokens;
} }
/**
* Return the operator matching the given query, or an empty string if none matches
*
* @param String $query The query to extract the operator from
* @param integer $i The offset to use in the query string
*
* @return string The operator string that matches best
*/
private function getMatchingOperator($query, $i) private function getMatchingOperator($query, $i)
{ {
$operatorToUse = ''; $operatorToUse = '';
@ -118,13 +209,25 @@ class UrlViewFilter
} }
} }
} }
return $operatorToUse; return $operatorToUse;
} }
/**
* Parse a new expression until the next conjunction or end and return the matching token for it
*
* @param String $query The query string to create a token from
* @param Integer $currentPos The offset to use in the query string
* @param array $tokenList The existing token list to add the token to
*
* @return array A two element array with the new offset in the beginning and the new
* parse state as the second parameter
*/
private function parseTarget($query, $currentPos, array &$tokenList) private function parseTarget($query, $currentPos, array &$tokenList)
{ {
$conjunctions = array('&', '|'); $conjunctions = array('&', '|');
$i = $currentPos; $i = $currentPos;
for ($i; $i < strlen($query); $i++) { for ($i; $i < strlen($query); $i++) {
$currentChar = $query[$i]; $currentChar = $query[$i];
// test if operator matches // test if operator matches
@ -134,7 +237,7 @@ class UrlViewFilter
// without value to the tokenlist // without value to the tokenlist
if ($operator !== '') { if ($operator !== '') {
$tokenList[] = array( $tokenList[] = array(
self::FILTER_TARGET => urldecode(substr($query, $currentPos, $i - $currentPos)), self::FILTER_TARGET => substr($query, $currentPos, $i - $currentPos),
self::FILTER_OPERATOR => $operator self::FILTER_OPERATOR => $operator
); );
// -1 because we're currently pointing at the first character of the operator // -1 because we're currently pointing at the first character of the operator
@ -153,7 +256,7 @@ class UrlViewFilter
if (is_array($lastState)) { if (is_array($lastState)) {
$tokenList[] = array( $tokenList[] = array(
self::FILTER_TARGET => urldecode($lastState[self::FILTER_TARGET]), self::FILTER_TARGET => $lastState[self::FILTER_TARGET],
self::FILTER_OPERATOR => $lastState[self::FILTER_OPERATOR], self::FILTER_OPERATOR => $lastState[self::FILTER_OPERATOR],
); );
return $this->parseValue($query, $currentPos, $tokenList); return $this->parseValue($query, $currentPos, $tokenList);
@ -165,7 +268,18 @@ class UrlViewFilter
return array($i, self::FILTER_TARGET); return array($i, self::FILTER_TARGET);
} }
/**
* Parse the value part of a query string, starting at current pos
*
* This expects an token without value to be placed in the tokenList stack
*
* @param String $query The query string to create a token from
* @param Integer $currentPos The offset to use in the query string
* @param array $tokenList The existing token list to add the token to
*
* @return array A two element array with the new offset in the beginning and the new
* parse state as the second parameter
*/
private function parseValue($query, $currentPos, array &$tokenList) private function parseValue($query, $currentPos, array &$tokenList)
{ {
@ -190,7 +304,7 @@ class UrlViewFilter
array_pop($tokenList); array_pop($tokenList);
return array($currentPos, self::FILTER_TARGET); return array($currentPos, self::FILTER_TARGET);
} }
$lastState[self::FILTER_VALUE] = urldecode(substr($query, $currentPos, $length)); $lastState[self::FILTER_VALUE] = substr($query, $currentPos, $length);
if (in_array($currentChar, $conjunctions)) { if (in_array($currentChar, $conjunctions)) {
$tokenList[] = $currentChar; $tokenList[] = $currentChar;
@ -198,7 +312,16 @@ class UrlViewFilter
return array($i, self::FILTER_TARGET); return array($i, self::FILTER_TARGET);
} }
private function skip($query, $currentPos, array &$tokenList) /**
* Skip a query substring until the next conjunction appears
*
* @param String $query The query string to skip the next token
* @param Integer $currentPos The offset to use in the query string
*
* @return array A two element array with the new offset in the beginning and the new
* parse state as the second parameter
*/
private function skip($query, $currentPos)
{ {
$conjunctions = array('&', '|'); $conjunctions = array('&', '|');
for ($i = $currentPos; strlen($query); $i++) { for ($i = $currentPos; strlen($query); $i++) {
@ -208,6 +331,4 @@ class UrlViewFilter
} }
} }
} }
} }

View File

@ -16,7 +16,6 @@ class HoststatusView extends AbstractView
'host_address', 'host_address',
'host_ipv4', 'host_ipv4',
'host_icon_image', 'host_icon_image',
// Hoststatus // Hoststatus
'host_state', 'host_state',
'host_problem', 'host_problem',
@ -39,7 +38,6 @@ class HoststatusView extends AbstractView
'host_last_time_unreachable', 'host_last_time_unreachable',
'host_current_check_attempt', 'host_current_check_attempt',
'host_max_check_attempts', 'host_max_check_attempts',
// Services // Services
'services_cnt', 'services_cnt',
'services_problem', 'services_problem',

View File

@ -2,12 +2,17 @@
namespace Test\Monitoring\Application\Controllers\ListController; namespace Test\Monitoring\Application\Controllers\ListController;
require_once(dirname(__FILE__).'/../../testlib/MonitoringControllerTest.php');
require_once(dirname(__FILE__).'/../../../../library/Monitoring/DataView/DataView.php'); require_once realpath(__DIR__ . '/../../../../../../library/Icinga/Test/BaseTestCase.php');
require_once(dirname(__FILE__).'/../../../../library/Monitoring/DataView/HostAndServiceStatus.php');
require_once(dirname(__FILE__).'/../../../../library/Monitoring/DataView/Notification.php'); use Icinga\Test\BaseTestCase;
require_once(dirname(__FILE__).'/../../../../library/Monitoring/DataView/Downtime.php');
require_once(realpath(BaseTestCase::$moduleDir . '/monitoring/test/php/testlib/MonitoringControllerTest.php'));
require_once(realpath(BaseTestCase::$moduleDir . '/monitoring/library/Monitoring/Filter/Backend/IdoQueryConverter.php'));
require_once(realpath(BaseTestCase::$moduleDir . '/monitoring/library/Monitoring/DataView/DataView.php'));
require_once(realpath(BaseTestCase::$moduleDir . '/monitoring/library/Monitoring/DataView/HostStatus.php'));
require_once(realpath(BaseTestCase::$moduleDir . '/monitoring/library/Monitoring/DataView/Notification.php'));
require_once(realpath(BaseTestCase::$moduleDir . '/monitoring/library/Monitoring/DataView/Downtime.php'));
use Test\Monitoring\Testlib\MonitoringControllerTest; use Test\Monitoring\Testlib\MonitoringControllerTest;
use Test\Monitoring\Testlib\Datasource\TestFixture; use Test\Monitoring\Testlib\Datasource\TestFixture;

View File

@ -2,7 +2,16 @@
namespace Test\Monitoring\Application\Controllers\ListController; namespace Test\Monitoring\Application\Controllers\ListController;
require_once(dirname(__FILE__).'/../../testlib/MonitoringControllerTest.php'); use Icinga\Test\BaseTestCase;
require_once realpath(__DIR__ . '/../../../../../../library/Icinga/Test/BaseTestCase.php');
require_once(realpath(BaseTestCase::$moduleDir . '/monitoring/test/php/testlib/MonitoringControllerTest.php'));
require_once(realpath(BaseTestCase::$moduleDir . '/monitoring/library/Monitoring/Filter/Backend/IdoQueryConverter.php'));
require_once(realpath(BaseTestCase::$moduleDir . '/monitoring/library/Monitoring/DataView/DataView.php'));
require_once(realpath(BaseTestCase::$moduleDir . '/monitoring/library/Monitoring/DataView/ServiceStatus.php'));
require_once(realpath(BaseTestCase::$moduleDir . '/monitoring/library/Monitoring/DataView/Notification.php'));
require_once(realpath(BaseTestCase::$moduleDir . '/monitoring/library/Monitoring/DataView/Downtime.php'));
use Test\Monitoring\Testlib\MonitoringControllerTest; use Test\Monitoring\Testlib\MonitoringControllerTest;
use Test\Monitoring\Testlib\Datasource\TestFixture; use Test\Monitoring\Testlib\Datasource\TestFixture;

View File

@ -27,6 +27,8 @@
// {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}}
namespace Test\Modules\Monitoring\Library\Filter; namespace Test\Modules\Monitoring\Library\Filter;
use Icinga\Filter\Filterable;
use Icinga\Filter\Query\Tree;
use Icinga\Module\Monitoring\Filter\Type\StatusFilter; use Icinga\Module\Monitoring\Filter\Type\StatusFilter;
use Icinga\Filter\Type\TimeRangeSpecifier; use Icinga\Filter\Type\TimeRangeSpecifier;
use Icinga\Filter\Query\Node; use Icinga\Filter\Query\Node;
@ -34,7 +36,6 @@ use Icinga\Filter\Filter;
use Icinga\Filter\Type\TextFilter; use Icinga\Filter\Type\TextFilter;
use Icinga\Filter\FilterAttribute; use Icinga\Filter\FilterAttribute;
use Icinga\Module\Monitoring\Filter\UrlViewFilter; use Icinga\Module\Monitoring\Filter\UrlViewFilter;
use Icinga\Protocol\Ldap\Exception;
use Icinga\Test\BaseTestCase; use Icinga\Test\BaseTestCase;
// @codingStandardsIgnoreStart // @codingStandardsIgnoreStart
@ -51,6 +52,25 @@ require_once realpath(BaseTestCase::$libDir .'/Filter/Type/TimeRangeSpecifier.ph
require_once realpath(BaseTestCase::$moduleDir .'/monitoring/library/Monitoring/Filter/Type/StatusFilter.php'); require_once realpath(BaseTestCase::$moduleDir .'/monitoring/library/Monitoring/Filter/Type/StatusFilter.php');
require_once realpath(BaseTestCase::$moduleDir .'/monitoring/library/Monitoring/Filter/UrlViewFilter.php'); require_once realpath(BaseTestCase::$moduleDir .'/monitoring/library/Monitoring/Filter/UrlViewFilter.php');
class FilterMock implements Filterable
{
public function isValidFilterTarget($field)
{
return true;
}
public function getMappedField($field)
{
return $field;
}
public function applyFilter(Tree $filter)
{
return true;
}
}
class UrlViewFilterTest extends BaseTestCase class UrlViewFilterTest extends BaseTestCase
{ {
@ -81,7 +101,7 @@ class UrlViewFilterTest extends BaseTestCase
. ' and attr5 is UP'; . ' and attr5 is UP';
$tree = $searchEngine->createQueryTreeForFilter($query); $tree = $searchEngine->createQueryTreeForFilter($query);
$filterFactory = new UrlViewFilter(); $filterFactory = new UrlViewFilter(new FilterMock());
$uri = $filterFactory->fromTree($tree); $uri = $filterFactory->fromTree($tree);
$this->assertEquals( $this->assertEquals(
'attr1!=Hans+wurst|attr2=%2Asomething%2A&attr3=%2Abla|attr4=1&host_last_state_change>=yesterday&attr5=0', 'attr1!=Hans+wurst|attr2=%2Asomething%2A&attr3=%2Abla|attr4=1&host_last_state_change>=yesterday&attr5=0',
@ -92,7 +112,7 @@ class UrlViewFilterTest extends BaseTestCase
public function testTreeFromSimpleKeyValueUrlCreation() public function testTreeFromSimpleKeyValueUrlCreation()
{ {
$filterFactory = new UrlViewFilter(); $filterFactory = new UrlViewFilter(new FilterMock());
$tree = $filterFactory->parseUrl('attr1!=Hans+Wurst'); $tree = $filterFactory->parseUrl('attr1!=Hans+Wurst');
$this->assertEquals( $this->assertEquals(
$tree->root->type, $tree->root->type,
@ -118,7 +138,7 @@ class UrlViewFilterTest extends BaseTestCase
public function testConjunctionFilterInUrl() public function testConjunctionFilterInUrl()
{ {
$filterFactory = new UrlViewFilter(); $filterFactory = new UrlViewFilter(new FilterMock());
$query = 'attr1!=Hans+Wurst&test=test123|bla=1'; $query = 'attr1!=Hans+Wurst&test=test123|bla=1';
$tree = $filterFactory->parseUrl($query); $tree = $filterFactory->parseUrl($query);
$this->assertEquals($tree->root->type, Node::TYPE_AND, 'Assert the root of the filter tree to be an AND node'); $this->assertEquals($tree->root->type, Node::TYPE_AND, 'Assert the root of the filter tree to be an AND node');
@ -127,7 +147,7 @@ class UrlViewFilterTest extends BaseTestCase
public function testImplicitConjunctionInUrl() public function testImplicitConjunctionInUrl()
{ {
$filterFactory = new UrlViewFilter(); $filterFactory = new UrlViewFilter(new FilterMock());
$query = 'attr1!=Hans+Wurst&test=test123|bla=1|2|3'; $query = 'attr1!=Hans+Wurst&test=test123|bla=1|2|3';
$tree = $filterFactory->parseUrl($query); $tree = $filterFactory->parseUrl($query);
$this->assertEquals($tree->root->type, Node::TYPE_AND, 'Assert the root of the filter tree to be an AND node'); $this->assertEquals($tree->root->type, Node::TYPE_AND, 'Assert the root of the filter tree to be an AND node');
@ -140,7 +160,7 @@ class UrlViewFilterTest extends BaseTestCase
public function testMissingValuesInQueries() public function testMissingValuesInQueries()
{ {
$filterFactory = new UrlViewFilter(); $filterFactory = new UrlViewFilter(new FilterMock());
$queryStr = 'attr1!=Hans+Wurst&test='; $queryStr = 'attr1!=Hans+Wurst&test=';
$tree = $filterFactory->parseUrl($queryStr); $tree = $filterFactory->parseUrl($queryStr);
$query = $filterFactory->fromTree($tree); $query = $filterFactory->fromTree($tree);
@ -149,7 +169,7 @@ class UrlViewFilterTest extends BaseTestCase
public function testErrorInQueries() public function testErrorInQueries()
{ {
$filterFactory = new UrlViewFilter(); $filterFactory = new UrlViewFilter(new FilterMock());
$queryStr = 'test=&attr1!=Hans+Wurst'; $queryStr = 'test=&attr1!=Hans+Wurst';
$tree = $filterFactory->parseUrl($queryStr); $tree = $filterFactory->parseUrl($queryStr);
$query = $filterFactory->fromTree($tree); $query = $filterFactory->fromTree($tree);
@ -158,7 +178,7 @@ class UrlViewFilterTest extends BaseTestCase
public function testSenselessConjunctions() public function testSenselessConjunctions()
{ {
$filterFactory = new UrlViewFilter(); $filterFactory = new UrlViewFilter(new FilterMock());
$queryStr = 'test=&|/5/|&attr1!=Hans+Wurst'; $queryStr = 'test=&|/5/|&attr1!=Hans+Wurst';
$tree = $filterFactory->parseUrl($queryStr); $tree = $filterFactory->parseUrl($queryStr);
$query = $filterFactory->fromTree($tree); $query = $filterFactory->fromTree($tree);
@ -168,7 +188,7 @@ class UrlViewFilterTest extends BaseTestCase
public function testRandomString() public function testRandomString()
{ {
$filter = ''; $filter = '';
$filterFactory = new UrlViewFilter(); $filterFactory = new UrlViewFilter(new FilterMock());
for ($i=0; $i<10;$i++) { for ($i=0; $i<10;$i++) {
$filter .= str_shuffle('&|ds& wra =!<>|dsgs=,-G'); $filter .= str_shuffle('&|ds& wra =!<>|dsgs=,-G');

View File

@ -162,6 +162,8 @@ abstract class MonitoringControllerTest extends Zend_Test_PHPUnit_ControllerTest
require_once('Data/Db/Query.php'); require_once('Data/Db/Query.php');
require_once('Exception/ProgrammingError.php'); require_once('Exception/ProgrammingError.php');
require_once('Web/Widget/SortBox.php'); require_once('Web/Widget/SortBox.php');
require_once('Web/Widget/FilterBox.php');
require_once('Web/Widget/FilterBadgeRenderer.php');
require_once('library/Monitoring/Backend/AbstractBackend.php'); require_once('library/Monitoring/Backend/AbstractBackend.php');
require_once('library/Monitoring/Backend.php'); require_once('library/Monitoring/Backend.php');

View File

@ -1,3 +1,4 @@
/*global Icinga:false, document: false, define:false require:false base_url:false console:false */
// {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}}
/** /**
* This file is part of Icinga 2 Web. * This file is part of Icinga 2 Web.
@ -24,50 +25,55 @@
* @author Icinga Development Team <info@icinga.org> * @author Icinga Development Team <info@icinga.org>
*/ */
// {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}}
/*global Icinga:false, document: false, define:false require:false base_url:false console:false */
/** /**
* Ensures that our date/time controls will work on every browser (natively or javascript based) * Ensures that our date/time controls will work on every browser (natively or javascript based)
*/ */
define(['jquery', 'logging', 'URIjs/URI'], function($, log, URI) { define(['jquery', 'logging', 'URIjs/URI', 'components/app/container'], function($, log, URI, Container) {
'use strict'; 'use strict';
return function(inputDOM) { return function(inputDOM) {
this.inputDom = $(inputDOM); this.inputDom = $(inputDOM);
this.form = this.inputDom.parents('form').first(); this.domain = this.inputDom.attr('data-icinga-filter-domain');
this.module = this.inputDom.attr('data-icinga-filter-module');
this.form = $(this.inputDom.parents('form').first());
this.formUrl = URI(this.form.attr('action')); this.formUrl = URI(this.form.attr('action'));
this.lastTokens = [];
this.lastQueuedEvent = null; this.lastQueuedEvent = null;
this.pendingRequest = null; this.pendingRequest = null;
/**
* Register the input listener
*/
this.construct = function() { this.construct = function() {
this.registerControlListener(); this.registerControlListener();
}; };
/**
* Request new proposals for the given input box
*/
this.getProposal = function() { this.getProposal = function() {
var text = this.inputDom.val().trim(); var text = this.inputDom.val().trim();
try {
if (this.pendingRequest) { if (this.pendingRequest) {
this.pendingRequest.abort(); this.pendingRequest.abort();
} }
this.pendingRequest = $.ajax({ this.pendingRequest = $.ajax(this.getRequestParams(text))
data: { .done(this.showProposals.bind(this))
'cache' : (new Date()).getTime(), .fail(this.showError.bind(this));
'query' : text
},
headers: {
'Accept': 'application/json'
},
url: this.formUrl
}).done(this.showProposals.bind(this)).fail(function() {});
} catch(exception) {
console.log(exception);
}
}; };
/**
* Apply a selected proposal to the text box
*
* String parts encapsulated in {} are parts that already exist in the input
*
* @param token The selected token
*/
this.applySelectedProposal = function(token) { this.applySelectedProposal = function(token) {
var currentText = $.trim(this.inputDom.val()); var currentText = $.trim(this.inputDom.val());
var substr = token.match(/^(\{.*\})/); var substr = token.match(/^(\{.*\})/);
if (substr !== null) { if (substr !== null) {
token = token.substr(substr[0].length); token = token.substr(substr[0].length);
@ -81,23 +87,63 @@ define(['jquery', 'logging', 'URIjs/URI'], function($, log, URI) {
this.inputDom.focus(); this.inputDom.focus();
}; };
this.showProposals = function(tokens, state, args) { /**
* Display an error in the box if the request failed
var jsonRep = args.responseText; *
* @param {Object} error The error response
* @param {String} state The HTTP state as a string
if (tokens.length === 0) { */
return this.inputDom.popover('destroy'); this.showError = function(error, state) {
if (state === 'abort') {
return;
}
this.inputDom.popover('destroy').popover({
content: '<div class="alert alert-danger"> ' + error.message + ' </div>',
html: true,
trigger: 'manual'
}).popover('show');
};
/**
* Return an Object containing the request information for the given query
*
* @param query
* @returns {{data: {cache: number, query: *, filter_domain: (*|Function|Function), filter_module: Function}, headers: {Accept: string}, url: *}}
*/
this.getRequestParams = function(query) {
return {
data: {
'cache' : (new Date()).getTime(),
'query' : query,
'filter_domain' : this.domain,
'filter_module' : this.module
},
headers: {
'Accept': 'application/json'
},
url: this.formUrl
};
};
/**
* Callback that renders the proposal list after retrieving it from the server
*
* @param {Object} response The jquery response object inheritn XHttpResponse Attributes
*/
this.showProposals = function(response) {
if (response.proposals.length === 0) {
this.inputDom.popover('destroy');
return;
} }
this.lastTokens = jsonRep;
var list = $('<ul>').addClass('nav nav-stacked nav-pills'); var list = $('<ul>').addClass('nav nav-stacked nav-pills');
$.each(tokens, (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>').
append($('<a href="#">'). append($('<a href="#">').
text(displayToken) text(displayToken)
).appendTo(list); ).appendTo(list);
proposal.on('click', (function(ev) { proposal.on('click', (function(ev) {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
@ -114,15 +160,58 @@ define(['jquery', 'logging', 'URIjs/URI'], function($, log, URI) {
}).popover('show'); }).popover('show');
}; };
/**
* Callback to update the current container with the entered url if it's valid
*/
this.updateFilter = function() {
var query = $.trim(this.inputDom.val());
this.pendingRequest = $.ajax(this.getRequestParams(query))
.done((function(response) {
var container = new Container($(this.inputDom));
var url = container.getContainerHref();
url += ( url.indexOf('?') === -1 ? '?' : '&' ) + response.urlParam;
container.replaceDomFromUrl(url);
}).bind(this));
};
/**
* Register listeners for the searchbox
*
* This means:
* - Activate/Deactivate the popover on focus and blur
* - Add Url tokens and submit on enter
*/
this.registerControlListener = function() { this.registerControlListener = function() {
this.inputDom.on('blur', (function() { this.inputDom.on('blur', (function() {
$(this).popover('hide'); $(this).popover('hide');
})); }));
this.inputDom.on('focus', updateProposalList.bind(this)); this.inputDom.on('focus', updateProposalList.bind(this));
this.inputDom.on('keyup', updateProposalList.bind(this)); this.inputDom.on('keyup', updateProposalList.bind(this));
this.inputDom.on('keydown', (function(keyEv) {
if ((keyEv.keyCode || keyEv.which) === 13) {
this.updateFilter();
keyEv.stopPropagation();
keyEv.preventDefault();
return false;
}
}).bind(this));
this.form.submit(function(ev) {
ev.stopPropagation();
ev.preventDefault();
return false;
});
}; };
var updateProposalList = function() { /**
* Callback to update the proposal list if a slight delay on keyPress
*
* Needs to be bound to the object scope
*
* @param {jQuery.Event} keyEv The key Event to react on
*/
var updateProposalList = function(keyEv) {
if (this.lastQueuedEvent) { if (this.lastQueuedEvent) {
window.clearTimeout(this.lastQueuedEvent); window.clearTimeout(this.lastQueuedEvent);
} }
@ -131,6 +220,4 @@ define(['jquery', 'logging', 'URIjs/URI'], function($, log, URI) {
this.construct(); this.construct();
}; };
}); });

View File

@ -294,4 +294,5 @@ class FilterTest extends BaseTestCase
'Assert the root->right->right->type node to be an OPERATOR (query :"' . $query . '")' 'Assert the root->right->right->type node to be an OPERATOR (query :"' . $query . '")'
); );
} }
} }

View File

@ -14,7 +14,7 @@ class StatusdatTestLoader extends LibraryLoader
require_once 'Zend/Log.php'; require_once 'Zend/Log.php';
require_once($libPath."/Data/AbstractQuery.php"); require_once($libPath."/Data/AbstractQuery.php");
require_once($libPath."/Application/Logger.php"); require_once($libPath."/Application/Logger.php");
require_once($libPath."/Filter/Filterable.php");
require_once($libPath."/Data/DatasourceInterface.php"); require_once($libPath."/Data/DatasourceInterface.php");
$statusdat = realpath($libPath."/Protocol/Statusdat/"); $statusdat = realpath($libPath."/Protocol/Statusdat/");
require_once($statusdat."/View/AccessorStrategy.php"); require_once($statusdat."/View/AccessorStrategy.php");