From ed4d24e1656704e9b62eee3838edfd063c41818e Mon Sep 17 00:00:00 2001 From: David Foster Date: Fri, 22 Jun 2012 10:47:42 -0700 Subject: [PATCH 01/12] Add base Endpoint, Collection, and Entity classes. Add initial collection for saved searches. --- Splunk/Collection.php | 29 ++++++++++++++++++++++++ Splunk/Context.php | 36 ++++++++++++++++++++++++++++-- Splunk/Endpoint.php | 33 +++++++++++++++++++++++++++ Splunk/Entity.php | 25 +++++++++++++++++++++ Splunk/Http.php | 21 ++++++++++------- Splunk/Service.php | 37 ++++++++++++++++++++++++++++++ tests/SavedSearchTest.php | 47 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 218 insertions(+), 10 deletions(-) create mode 100644 Splunk/Collection.php create mode 100644 Splunk/Endpoint.php create mode 100644 Splunk/Entity.php create mode 100644 Splunk/Service.php create mode 100644 tests/SavedSearchTest.php diff --git a/Splunk/Collection.php b/Splunk/Collection.php new file mode 100644 index 0000000..2d443d4 --- /dev/null +++ b/Splunk/Collection.php @@ -0,0 +1,29 @@ +service, "{$this->path}{$name}"); + } +} \ No newline at end of file diff --git a/Splunk/Context.php b/Splunk/Context.php index c554093..6104257 100644 --- a/Splunk/Context.php +++ b/Splunk/Context.php @@ -16,7 +16,7 @@ */ /** - * This class allows clients to issue HTTP requests to a Splunk server. + * Allows clients to issue HTTP requests to a Splunk server. * * @package Splunk */ @@ -79,6 +79,20 @@ public function login() $this->token = 'Splunk ' . $sessionKey; } + /** + * Performs an HTTP GET request to the endpoint at the specified path. + * + * @param string $path relative or absolute URL path. + * @return array + * @see Splunk_Http::get + */ + public function get($path) + { + return $this->http->get($this->url($path), array( + 'Authorization' => $this->token, + )); + } + // === Accessors === /** @@ -92,8 +106,26 @@ public function getToken() // === Utility === + /** + * @param string $path relative or absolute URL path. + * @return string absolute URL. + */ private function url($path) { - return "{$this->scheme}://{$this->host}:{$this->port}{$path}"; + return "{$this->scheme}://{$this->host}:{$this->port}{$this->abspath($path)}"; + } + + /** + * @param string $path relative or absolute URL path. + * @return string absolute URL path. + */ + private function abspath($path) + { + if ((strlen($path) >= 1) && ($path[0] == '/')) + return $path; + + // TODO: Support namespaces + // TODO: Quote the path component properly. + return "/services/{$path}"; } } diff --git a/Splunk/Endpoint.php b/Splunk/Endpoint.php new file mode 100644 index 0000000..884ae37 --- /dev/null +++ b/Splunk/Endpoint.php @@ -0,0 +1,33 @@ +service = $service; + $this->path = $path; + } +} \ No newline at end of file diff --git a/Splunk/Entity.php b/Splunk/Entity.php new file mode 100644 index 0000000..18df66f --- /dev/null +++ b/Splunk/Entity.php @@ -0,0 +1,25 @@ +request('get', $url); + return $this->request('get', $url, $request_headers); } public function post($url, $params=array()) { - return $this->request('post', $url, http_build_query($params)); + return $this->request('post', $url, array(), http_build_query($params)); } /** - * @param string $method HTTP request method (ex: 'get'). - * @param string $url URL to fetch. - * @param string $request_body content to send in the request. - * @return object { + * @param string $method HTTP request method (ex: 'get'). + * @param string $url URL to fetch. + * @param array $request_headers dictionary of header names and values. + * @param string $request_body content to send in the request. + * @return array { * 'status' => HTTP status code (ex: 200). * 'reason' => HTTP reason string (ex: 'OK'). * 'headers' => Dictionary of headers. (ex: array('Content-Length' => '0')). * 'body' => Content of the response. * } */ - private function request($method, $url, $request_body='') + private function request( + $method, $url, $request_headers=array(), $request_body='') { $opts = array( CURLOPT_HTTPGET => TRUE, @@ -55,6 +57,9 @@ private function request($method, $url, $request_body='') CURLOPT_SSL_VERIFYPEER => FALSE, ); + foreach ($request_headers as $k => $v) + $opts[CURLOPT_HTTPHEADER][] = "$k: $v"; + switch ($method) { case 'get': diff --git a/Splunk/Service.php b/Splunk/Service.php new file mode 100644 index 0000000..c049cb1 --- /dev/null +++ b/Splunk/Service.php @@ -0,0 +1,37 @@ +login(); + + $response = $context->get('/servicesNS/nobody/search/saved/searches/'); + $this->assertContains( + '' . self::SAVED_SEARCH_NAME . '', + $response['body']); + } + + public function testGetSavedSearch() + { + global $Splunk_testSettings; + $service = new Splunk_Service($Splunk_testSettings['connectArgs']); + + $savedSearch = $service->savedSearches()->get(self::SAVED_SEARCH_NAME); + return $savedSearch; + } +} \ No newline at end of file From 110aa9abf518b48fbc868c7bed552869a57da957 Mon Sep 17 00:00:00 2001 From: David Foster Date: Mon, 25 Jun 2012 17:10:55 -0700 Subject: [PATCH 02/12] Collections list contents. Entities load their content. --- Splunk/AmbiguousKeyException.php | 21 +++++++ Splunk/AtomFeed.php | 57 +++++++++++++++++++ Splunk/Collection.php | 71 +++++++++++++++++++++++- Splunk/Entity.php | 41 +++++++++++++- Splunk/Http.php | 2 + Splunk/NoSuchKeyException.php | 21 +++++++ Splunk/UnsupportedOperationException.php | 25 +++++++++ Splunk/Util.php | 2 +- tests/SavedSearchTest.php | 11 ++++ 9 files changed, 247 insertions(+), 4 deletions(-) create mode 100644 Splunk/AmbiguousKeyException.php create mode 100644 Splunk/AtomFeed.php create mode 100644 Splunk/NoSuchKeyException.php create mode 100644 Splunk/UnsupportedOperationException.php diff --git a/Splunk/AmbiguousKeyException.php b/Splunk/AmbiguousKeyException.php new file mode 100644 index 0000000..e22f04f --- /dev/null +++ b/Splunk/AmbiguousKeyException.php @@ -0,0 +1,21 @@ +children(Splunk_AtomFeed::NS_S)->dict; + + if (Splunk_AtomFeed::elementExists($dictValue)) + { + return Splunk_AtomFeed::parseDict($dictValue); + } + else // value is scalar + { + return Splunk_Util::getTextContentOfXmlElement($containerXml); + } + } + + private static function parseDict($dictXml) + { + $dict = array(); + foreach ($dictXml->children(Splunk_AtomFeed::NS_S)->key as $keyXml) + { + $key = (string) $keyXml->attributes()->name; + $value = Splunk_AtomFeed::parseValueInside($keyXml); + + $dict[$key] = $value; + } + return $dict; + } + + private static function elementExists($xml) + { + return $xml->getName() != ''; + } +} \ No newline at end of file diff --git a/Splunk/Collection.php b/Splunk/Collection.php index 2d443d4..d1fda52 100644 --- a/Splunk/Collection.php +++ b/Splunk/Collection.php @@ -22,8 +22,75 @@ */ class Splunk_Collection extends Splunk_Endpoint { + private $loaded = FALSE; + private $entries = NULL; + + /** Loads this collection if not already done. Returns self. */ + private function validate() + { + if (!$this->loaded) + { + $this->load(); + } + return $this; + } + + private function load() + { + $response = $this->service->get($this->path); + $xml = new SimpleXMLElement($response['body']); + + $entries = array(); + foreach ($xml->entry as $entryData) + { + $entries[] = $this->loadEntry($entryData); + } + + $this->entries = $entries; + $this->loaded = TRUE; + } + + private function loadEntry($entryData) + { + return new Splunk_Entity( + $this->service, + "{$this->path}/{$entryData->title}", + $entryData); + } + + /** + * Returns the unique entity with the specified name. + * + * @param string $name + * @return Splunk_Entity + * @throws Splunk_NoSuchKeyException + * @throws Splunk_AmbiguousKeyException + */ public function get($name) { - return new Splunk_Entity($this->service, "{$this->path}{$name}"); + $results = array(); + foreach ($this->validate()->entries as $entry) + { + if ($entry->getName() == $name) + { + $results[] = $entry; + } + } + + if (count($results) == 0) + { + throw new Splunk_NoSuchKeyException( + "No value exists with key '{$name}'."); + } + else if (count($results) == 1) + { + return $results[0]; + } + else + { + throw new Splunk_AmbiguousKeyException( + "Multiple values exist with key '{$name}'. " . + "Specify a namespace to disambiguate."); + } } -} \ No newline at end of file +} diff --git a/Splunk/Entity.php b/Splunk/Entity.php index 18df66f..354b527 100644 --- a/Splunk/Entity.php +++ b/Splunk/Entity.php @@ -20,6 +20,45 @@ * * @package Splunk */ -class Splunk_Entity extends Splunk_Endpoint +class Splunk_Entity extends Splunk_Endpoint implements ArrayAccess { + private $data; + private $content; + + public function __construct($service, $path, $data) + { + parent::__construct($service, $path); + $this->data = $data; + + $this->content = Splunk_AtomFeed::parseValueInside($data->content); + } + + // === Accessors === + + public function getName() + { + return (string) $this->data->title; + } + + // === ArrayAccess Methods === + + public function offsetGet($key) + { + return $this->content[$key]; + } + + public function offsetSet($key, $value) + { + throw new Splunk_UnsupportedOperationException(); + } + + public function offsetUnset($key) + { + throw new Splunk_UnsupportedOperationException(); + } + + public function offsetExists($key) + { + return isset($this->content[$key]); + } } \ No newline at end of file diff --git a/Splunk/Http.php b/Splunk/Http.php index 6d0016f..3d2c6b7 100644 --- a/Splunk/Http.php +++ b/Splunk/Http.php @@ -43,6 +43,8 @@ public function post($url, $params=array()) * 'headers' => Dictionary of headers. (ex: array('Content-Length' => '0')). * 'body' => Content of the response. * } + * @throws Splunk_ConnectException + * @throws Splunk_HttpException */ private function request( $method, $url, $request_headers=array(), $request_body='') diff --git a/Splunk/NoSuchKeyException.php b/Splunk/NoSuchKeyException.php new file mode 100644 index 0000000..93a6b2b --- /dev/null +++ b/Splunk/NoSuchKeyException.php @@ -0,0 +1,21 @@ +login(); $savedSearch = $service->savedSearches()->get(self::SAVED_SEARCH_NAME); return $savedSearch; } + + /** + * @depends testGetSavedSearch + */ + public function testGetPropertyOfSavedSearch($savedSearch) + { + $this->assertEquals( + self::SAVED_SEARCH_QUERY, + $savedSearch['search']); + } } \ No newline at end of file From 065758de60dfa251eccb44788a8142b60b28bc9e Mon Sep 17 00:00:00 2001 From: David Foster Date: Mon, 25 Jun 2012 17:30:06 -0700 Subject: [PATCH 03/12] Extract XML utilities to XmlUtil class. --- Splunk/AtomFeed.php | 11 ++----- Splunk/Context.php | 2 +- Splunk/Http.php | 2 +- Splunk/Util.php | 48 ------------------------------ Splunk/XmlUtil.php | 71 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 76 insertions(+), 58 deletions(-) delete mode 100644 Splunk/Util.php create mode 100644 Splunk/XmlUtil.php diff --git a/Splunk/AtomFeed.php b/Splunk/AtomFeed.php index 1bede50..5f19f49 100644 --- a/Splunk/AtomFeed.php +++ b/Splunk/AtomFeed.php @@ -27,13 +27,13 @@ public static function parseValueInside($containerXml) { $dictValue = $containerXml->children(Splunk_AtomFeed::NS_S)->dict; - if (Splunk_AtomFeed::elementExists($dictValue)) + if (Splunk_XmlUtil::elementExists($dictValue)) { return Splunk_AtomFeed::parseDict($dictValue); } else // value is scalar { - return Splunk_Util::getTextContentOfXmlElement($containerXml); + return Splunk_XmlUtil::getTextContent($containerXml); } } @@ -42,16 +42,11 @@ private static function parseDict($dictXml) $dict = array(); foreach ($dictXml->children(Splunk_AtomFeed::NS_S)->key as $keyXml) { - $key = (string) $keyXml->attributes()->name; + $key = Splunk_XmlUtil::getAttributeValue($keyXml, 'name'); $value = Splunk_AtomFeed::parseValueInside($keyXml); $dict[$key] = $value; } return $dict; } - - private static function elementExists($xml) - { - return $xml->getName() != ''; - } } \ No newline at end of file diff --git a/Splunk/Context.php b/Splunk/Context.php index 6104257..2f8feb0 100644 --- a/Splunk/Context.php +++ b/Splunk/Context.php @@ -72,7 +72,7 @@ public function login() 'password' => $this->password, )); - $sessionKey = Splunk_Util::getTextContentAtXpath( + $sessionKey = Splunk_XmlUtil::getTextContentAtXpath( new SimpleXMLElement($response['body']), '/response/sessionKey'); diff --git a/Splunk/Http.php b/Splunk/Http.php index 3d2c6b7..02140c8 100644 --- a/Splunk/Http.php +++ b/Splunk/Http.php @@ -147,7 +147,7 @@ public function __construct($response) private static function parseFirstMessageFrom($response) { - return Splunk_Util::getTextContentAtXpath( + return Splunk_XmlUtil::getTextContentAtXpath( new SimpleXMLElement($response['body']), '/response/messages/msg'); } diff --git a/Splunk/Util.php b/Splunk/Util.php deleted file mode 100644 index 4a696a4..0000000 --- a/Splunk/Util.php +++ /dev/null @@ -1,48 +0,0 @@ -xpath($xpathExpr); - return (count($matchingElements) == 0) - ? NULL - : Splunk_Util::getTextContentOfXmlElement($matchingElements[0]); - } - - /** - * @param SimpleXMLElement $xmlElement - * @return string - */ - public static function getTextContentOfXmlElement($xmlElement) - { - // HACK: Some versions of PHP 5 can't access the [0] element - // of a SimpleXMLElement object properly. - return (string) $xmlElement; - } -} diff --git a/Splunk/XmlUtil.php b/Splunk/XmlUtil.php new file mode 100644 index 0000000..565a5cc --- /dev/null +++ b/Splunk/XmlUtil.php @@ -0,0 +1,71 @@ +getName() != ''; + } + + /** + * @param SimpleXMLElement $xml + * @param string $attributeName + * @return string|NULL + */ + public static function getAttributeValue($xml, $attributeName) + { + return (isset($xml->attributes()->$attributeName)) + ? (string) $xml->attributes()->$attributeName + : NULL; + } + + /** + * @param SimpleXMLElement $xml + * @return string + */ + public static function getTextContent($xml) + { + // HACK: Some versions of PHP 5 can't access the [0] element + // of a SimpleXMLElement object properly. + return (string) $xml; + } + + /** + * @param SimpleXMLElement $xml + * @param string $xpathExpr + * @return string|NULL + */ + public static function getTextContentAtXpath($xml, $xpathExpr) + { + $matchingElements = $xml->xpath($xpathExpr); + return (count($matchingElements) == 0) + ? NULL + : Splunk_XmlUtil::getTextContent($matchingElements[0]); + } +} From 90a75a9ce6a0955c5482678e17579ea79da223e9 Mon Sep 17 00:00:00 2001 From: David Foster Date: Tue, 26 Jun 2012 13:07:22 -0700 Subject: [PATCH 04/12] Support login with a token. --- Splunk/Context.php | 10 +++++++--- tests/ContextTest.php | 11 ++++++++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Splunk/Context.php b/Splunk/Context.php index 2f8feb0..47436f9 100644 --- a/Splunk/Context.php +++ b/Splunk/Context.php @@ -24,17 +24,19 @@ class Splunk_Context { private $username; private $password; + private $token; private $host; private $port; private $scheme; private $http; - private $token; - /** * @param array $args { * 'username' => (optional) The username to login with. Defaults to "admin". * 'password' => (optional) The password to login with. Defaults to "changeme". + * 'token' => (optional) The authentication token to use. If provided, + * the username and password are ignored and there is no + * need to call login(). In the format "Splunk SESSION_KEY". * 'host' => (optional) The hostname of the Splunk server. Defaults to "localhost". * 'port' => (optional) The port of the Splunk server. Defaults to 8089. * 'scheme' => (optional) The scheme to use: either "http" or "https". Defaults to "https". @@ -46,6 +48,7 @@ public function __construct($args) $args = array_merge(array( 'username' => 'admin', 'password' => 'changeme', + 'token' => NULL, 'host' => 'localhost', 'port' => 8089, 'scheme' => 'https', @@ -54,6 +57,7 @@ public function __construct($args) $this->username = $args['username']; $this->password = $args['password']; + $this->token = $args['token']; $this->host = $args['host']; $this->port = $args['port']; $this->scheme = $args['scheme']; @@ -76,7 +80,7 @@ public function login() new SimpleXMLElement($response['body']), '/response/sessionKey'); - $this->token = 'Splunk ' . $sessionKey; + $this->token = "Splunk {$sessionKey}"; } /** diff --git a/tests/ContextTest.php b/tests/ContextTest.php index 4d71ccf..3b6fe51 100644 --- a/tests/ContextTest.php +++ b/tests/ContextTest.php @@ -39,8 +39,9 @@ public function testLoginSuccess() $context = new Splunk_Context(array( 'http' => $http, )); - $context->login(); + $this->assertEquals(NULL, $context->getToken()); + $context->login(); $this->assertEquals( 'Splunk 068b3021210eb4b67819b1a292302948', $context->getToken()); @@ -82,4 +83,12 @@ public function testLoginSuccessOnRealServer() $context = new Splunk_Context($Splunk_testSettings['connectArgs']); $context->login(); } + + public function testLoginWithToken() + { + $context = new Splunk_Context(array( + 'token' => 'Splunk ACEACE' + )); + $this->assertEquals('Splunk ACEACE', $context->getToken()); + } } From d0a09892d173cace57d5e1b8032ad277365faf9a Mon Sep 17 00:00:00 2001 From: David Foster Date: Tue, 26 Jun 2012 13:22:34 -0700 Subject: [PATCH 05/12] Alter Http::request() to return its response as an object instead of an array. --- Splunk/Collection.php | 2 +- Splunk/Context.php | 2 +- Splunk/Http.php | 9 ++++----- tests/ContextTest.php | 4 ++-- tests/HttpTest.php | 4 ++-- tests/SavedSearchTest.php | 2 +- 6 files changed, 11 insertions(+), 12 deletions(-) diff --git a/Splunk/Collection.php b/Splunk/Collection.php index d1fda52..3a4b3e4 100644 --- a/Splunk/Collection.php +++ b/Splunk/Collection.php @@ -38,7 +38,7 @@ private function validate() private function load() { $response = $this->service->get($this->path); - $xml = new SimpleXMLElement($response['body']); + $xml = new SimpleXMLElement($response->body); $entries = array(); foreach ($xml->entry as $entryData) diff --git a/Splunk/Context.php b/Splunk/Context.php index 47436f9..e231f9b 100644 --- a/Splunk/Context.php +++ b/Splunk/Context.php @@ -77,7 +77,7 @@ public function login() )); $sessionKey = Splunk_XmlUtil::getTextContentAtXpath( - new SimpleXMLElement($response['body']), + new SimpleXMLElement($response->body), '/response/sessionKey'); $this->token = "Splunk {$sessionKey}"; diff --git a/Splunk/Http.php b/Splunk/Http.php index 02140c8..198aef5 100644 --- a/Splunk/Http.php +++ b/Splunk/Http.php @@ -37,7 +37,7 @@ public function post($url, $params=array()) * @param string $url URL to fetch. * @param array $request_headers dictionary of header names and values. * @param string $request_body content to send in the request. - * @return array { + * @return object { * 'status' => HTTP status code (ex: 200). * 'reason' => HTTP reason string (ex: 'OK'). * 'headers' => Dictionary of headers. (ex: array('Content-Length' => '0')). @@ -100,7 +100,7 @@ private function request( list($http_version, $_, $reason) = explode(' ', $status_line, 3); - $response = array( + $response = (object) array( 'status' => $status, 'reason' => $reason, 'headers' => $headers, @@ -136,8 +136,7 @@ public function __construct($response) { $detail = Splunk_HttpException::parseFirstMessageFrom($response); - // FIXME: Include HTTP "reason" in message - $message = "HTTP {$response['status']} {$response['reason']}"; + $message = "HTTP {$response->status} {$response->reason}"; if ($detail != NULL) $message .= ' -- ' . $detail; @@ -148,7 +147,7 @@ public function __construct($response) private static function parseFirstMessageFrom($response) { return Splunk_XmlUtil::getTextContentAtXpath( - new SimpleXMLElement($response['body']), + new SimpleXMLElement($response->body), '/response/messages/msg'); } diff --git a/tests/ContextTest.php b/tests/ContextTest.php index 3b6fe51..ac9b21c 100644 --- a/tests/ContextTest.php +++ b/tests/ContextTest.php @@ -22,7 +22,7 @@ class ContextTest extends PHPUnit_Framework_TestCase { public function testLoginSuccess() { - $http_response = array( + $http_response = (object) array( 'status' => 200, 'reason' => 'OK', 'headers' => array(), @@ -53,7 +53,7 @@ public function testLoginSuccess() */ public function testLoginFailDueToBadPassword() { - $http_response = array( + $http_response = (object) array( 'status' => 401, 'reason' => 'Unauthorized', 'headers' => array(), diff --git a/tests/HttpTest.php b/tests/HttpTest.php index 4966c8b..3caeef5 100644 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -24,7 +24,7 @@ public function testGet() $http = new Splunk_Http(); $response = $http->get('http://www.splunk.com/'); - $this->assertEquals(200, $response['status']); - $this->assertContains('', $response['body']); + $this->assertEquals(200, $response->status); + $this->assertContains('', $response->body); } } diff --git a/tests/SavedSearchTest.php b/tests/SavedSearchTest.php index 5742743..25218a9 100644 --- a/tests/SavedSearchTest.php +++ b/tests/SavedSearchTest.php @@ -33,7 +33,7 @@ public function testGetSavedSearchCollectionUsingContext() $response = $context->get('/servicesNS/nobody/search/saved/searches/'); $this->assertContains( '' . self::SAVED_SEARCH_NAME . '', - $response['body']); + $response->body); } public function testGetSavedSearch() From 73c9597d774f5827554ca7cf01dec6d1e39be94e Mon Sep 17 00:00:00 2001 From: David Foster Date: Tue, 26 Jun 2012 13:32:29 -0700 Subject: [PATCH 06/12] Alter unit tests to be run from root directory. This allows the tests to import the PHP SDK in a standard fashion. --- tests/ContextTest.php | 2 +- tests/HttpTest.php | 2 +- tests/Makefile | 2 -- tests/README.md | 8 ++------ tests/SavedSearchTest.php | 2 +- 5 files changed, 5 insertions(+), 11 deletions(-) delete mode 100644 tests/Makefile diff --git a/tests/ContextTest.php b/tests/ContextTest.php index ac9b21c..9a59193 100644 --- a/tests/ContextTest.php +++ b/tests/ContextTest.php @@ -15,7 +15,7 @@ * under the License. */ -require_once '../Splunk.php'; +require_once 'Splunk.php'; require_once 'settings.php'; class ContextTest extends PHPUnit_Framework_TestCase diff --git a/tests/HttpTest.php b/tests/HttpTest.php index 3caeef5..61e3351 100644 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -15,7 +15,7 @@ * under the License. */ -require_once '../Splunk.php'; +require_once 'Splunk.php'; class HttpTest extends PHPUnit_Framework_TestCase { diff --git a/tests/Makefile b/tests/Makefile deleted file mode 100644 index 4db1caa..0000000 --- a/tests/Makefile +++ /dev/null @@ -1,2 +0,0 @@ -test: - phpunit . diff --git a/tests/README.md b/tests/README.md index 6603513..3123c62 100644 --- a/tests/README.md +++ b/tests/README.md @@ -4,10 +4,6 @@ ## Running the Tests -Navigate to this directory, then run: +Navigate to the root directory containing `Splunk.php`, then run: - phpunit . - -Or alternatively: - - make \ No newline at end of file + phpunit tests diff --git a/tests/SavedSearchTest.php b/tests/SavedSearchTest.php index 25218a9..8eb0990 100644 --- a/tests/SavedSearchTest.php +++ b/tests/SavedSearchTest.php @@ -15,7 +15,7 @@ * under the License. */ -require_once '../Splunk.php'; +require_once 'Splunk.php'; require_once 'settings.php'; class SavedSearchTest extends PHPUnit_Framework_TestCase From b912ce3dde737f27c9379f4216d09c95f04b3736 Mon Sep 17 00:00:00 2001 From: David Foster Date: Tue, 26 Jun 2012 13:56:33 -0700 Subject: [PATCH 07/12] Atom parser can handle lists. Add unit tests for the parser. --- Splunk/AtomFeed.php | 13 +++++ tests/AtomFeedTest.php | 126 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 tests/AtomFeedTest.php diff --git a/Splunk/AtomFeed.php b/Splunk/AtomFeed.php index 5f19f49..0be7295 100644 --- a/Splunk/AtomFeed.php +++ b/Splunk/AtomFeed.php @@ -26,11 +26,16 @@ class Splunk_AtomFeed public static function parseValueInside($containerXml) { $dictValue = $containerXml->children(Splunk_AtomFeed::NS_S)->dict; + $listValue = $containerXml->children(Splunk_AtomFeed::NS_S)->list; if (Splunk_XmlUtil::elementExists($dictValue)) { return Splunk_AtomFeed::parseDict($dictValue); } + else if (Splunk_XmlUtil::elementExists($listValue)) + { + return Splunk_AtomFeed::parseList($listValue); + } else // value is scalar { return Splunk_XmlUtil::getTextContent($containerXml); @@ -49,4 +54,12 @@ private static function parseDict($dictXml) } return $dict; } + + private static function parseList($listXml) + { + $list = array(); + foreach ($listXml->children(Splunk_AtomFeed::NS_S)->item as $itemXml) + $list[] = Splunk_AtomFeed::parseValueInside($itemXml); + return $list; + } } \ No newline at end of file diff --git a/tests/AtomFeedTest.php b/tests/AtomFeedTest.php new file mode 100644 index 0000000..6dea115 --- /dev/null +++ b/tests/AtomFeedTest.php @@ -0,0 +1,126 @@ +1'; + $expectedValue = 1; + + $this->checkParseResult($xmlString, $expectedValue); + } + + public function testParseDict() + { + $xmlString = ' + + + v1 + v2 + + '; + $expectedValue = array( + 'k1' => 'v1', + 'k2' => 'v2', + ); + + $this->checkParseResult($xmlString, $expectedValue); + } + + public function testParseList() + { + $xmlString = ' + + + e1 + e2 + + '; + $expectedValue = array( + 'e1', + 'e2', + ); + + $this->checkParseResult($xmlString, $expectedValue); + } + + public function testParseEmpty() + { + $xmlString = ''; + $expectedValue = ''; + + $this->checkParseResult($xmlString, $expectedValue); + } + + public function testParseComplex() + { + $xmlString = ' + + + 0 + + + + 1 + + + + + * + + + + + admin + + + + + 0 + app + + + + '; + $expectedValue = array( + 'action.email' => '0', + 'action.email.sendresults' => '', + 'eai:acl' => array( + 'can_write' => '1', + 'perms' => array( + 'read' => array('*'), + 'write' => array('admin'), + ), + 'removable' => '0', + 'sharing' => 'app', + ), + ); + + $this->checkParseResult($xmlString, $expectedValue); + } + + private function checkParseResult($xmlString, $expectedValue) + { + $this->assertEquals( + $expectedValue, + Splunk_AtomFeed::parseValueInside(new SimpleXMLElement($xmlString))); + } +} \ No newline at end of file From 0476e6efcaf8721aa587ffa892b17a884022554b Mon Sep 17 00:00:00 2001 From: David Foster Date: Tue, 26 Jun 2012 14:56:46 -0700 Subject: [PATCH 08/12] Load entities by name. Quote entity names in endpoint paths correctly. --- Splunk/Collection.php | 6 +++++- Splunk/Context.php | 1 - Splunk/Entity.php | 45 ++++++++++++++++++++++++++++++++++----- Splunk/Service.php | 9 +++++++- tests/SavedSearchTest.php | 44 ++++++++++++++++++++++++++++++++------ 5 files changed, 90 insertions(+), 15 deletions(-) diff --git a/Splunk/Collection.php b/Splunk/Collection.php index 3a4b3e4..aa0341a 100644 --- a/Splunk/Collection.php +++ b/Splunk/Collection.php @@ -25,6 +25,8 @@ class Splunk_Collection extends Splunk_Endpoint private $loaded = FALSE; private $entries = NULL; + // === Load === + /** Loads this collection if not already done. Returns self. */ private function validate() { @@ -54,10 +56,12 @@ private function loadEntry($entryData) { return new Splunk_Entity( $this->service, - "{$this->path}/{$entryData->title}", + "{$this->path}/" . urlencode($entryData->title), $entryData); } + // === Children === + /** * Returns the unique entity with the specified name. * diff --git a/Splunk/Context.php b/Splunk/Context.php index e231f9b..4b95768 100644 --- a/Splunk/Context.php +++ b/Splunk/Context.php @@ -129,7 +129,6 @@ private function abspath($path) return $path; // TODO: Support namespaces - // TODO: Quote the path component properly. return "/services/{$path}"; } } diff --git a/Splunk/Entity.php b/Splunk/Entity.php index 354b527..6fc9134 100644 --- a/Splunk/Entity.php +++ b/Splunk/Entity.php @@ -22,29 +22,64 @@ */ class Splunk_Entity extends Splunk_Endpoint implements ArrayAccess { + private $loaded = FALSE; private $data; private $content; - public function __construct($service, $path, $data) + /** + * @param Splunk_Service $service + * @param string $path + * @param SimpleXMLElement $data (optional) The XML of this entity, + * as received from the REST API. + */ + public function __construct($service, $path, $data=NULL) { parent::__construct($service, $path); + $this->data = $data; + if ($this->data != NULL) + $this->loadContentsOfData(); + } + + // === Load === + + /** Loads this entity if not already done. Returns self. */ + private function validate() + { + if (!$this->loaded) + { + $this->load(); + } + return $this; + } + + private function load() + { + $response = $this->service->get($this->path); + $xml = new SimpleXMLElement($response->body); - $this->content = Splunk_AtomFeed::parseValueInside($data->content); + $this->data = $xml->entry; + $this->loadContentsOfData(); + } + + private function loadContentsOfData() + { + $this->content = Splunk_AtomFeed::parseValueInside($this->data->content); + $this->loaded = TRUE; } // === Accessors === public function getName() { - return (string) $this->data->title; + return (string) $this->validate()->data->title; } // === ArrayAccess Methods === public function offsetGet($key) { - return $this->content[$key]; + return $this->validate()->content[$key]; } public function offsetSet($key, $value) @@ -59,6 +94,6 @@ public function offsetUnset($key) public function offsetExists($key) { - return isset($this->content[$key]); + return isset($this->validate()->content[$key]); } } \ No newline at end of file diff --git a/Splunk/Service.php b/Splunk/Service.php index c049cb1..558739f 100644 --- a/Splunk/Service.php +++ b/Splunk/Service.php @@ -30,7 +30,14 @@ public function __construct($args) parent::__construct($args); } - public function savedSearches() + // === Endpoints === + + public function getSavedSearch($name) + { + return new Splunk_Entity($this, 'saved/searches/' . urlencode($name)); + } + + public function getSavedSearches() { return new Splunk_Collection($this, 'saved/searches/'); } diff --git a/tests/SavedSearchTest.php b/tests/SavedSearchTest.php index 8eb0990..96fa602 100644 --- a/tests/SavedSearchTest.php +++ b/tests/SavedSearchTest.php @@ -36,23 +36,53 @@ public function testGetSavedSearchCollectionUsingContext() $response->body); } + public function testGetSavedSearchFromCollection() + { + global $Splunk_testSettings; + $service = new Splunk_Service($Splunk_testSettings['connectArgs']); + $service->login(); + + $savedSearch = $service->getSavedSearches()->get(self::SAVED_SEARCH_NAME); + return $savedSearch; + } + + /** @depends testGetSavedSearchFromCollection */ + public function testGetPropertyOfSavedSearchFromCollection($savedSearch) + { + $this->assertEquals(self::SAVED_SEARCH_QUERY, $savedSearch['search']); + } + public function testGetSavedSearch() { global $Splunk_testSettings; $service = new Splunk_Service($Splunk_testSettings['connectArgs']); $service->login(); - $savedSearch = $service->savedSearches()->get(self::SAVED_SEARCH_NAME); + $savedSearch = $service->getSavedSearch(self::SAVED_SEARCH_NAME); return $savedSearch; } - /** - * @depends testGetSavedSearch - */ + /** @depends testGetSavedSearch */ public function testGetPropertyOfSavedSearch($savedSearch) { - $this->assertEquals( - self::SAVED_SEARCH_QUERY, - $savedSearch['search']); + $this->assertEquals(self::SAVED_SEARCH_QUERY, $savedSearch['search']); + } + + public function testGetMissingSavedSearch() + { + global $Splunk_testSettings; + $service = new Splunk_Service($Splunk_testSettings['connectArgs']); + $service->login(); + + $savedSearch = $service->getSavedSearch('NO_SUCH_SEARCH'); + try + { + $savedSearch->getName(); // force load from server + $this->assertFail('Expected Splunk_HttpException to be thrown.'); + } + catch (Splunk_HttpException $e) + { + $this->assertEquals(404, $e->getResponse()->status); + } } } \ No newline at end of file From c91a7911005544cad668de871c7e2bf4be70da8b Mon Sep 17 00:00:00 2001 From: David Foster Date: Tue, 26 Jun 2012 15:03:54 -0700 Subject: [PATCH 09/12] Pull up common loading logic from {Collection, Entity} to Endpoint. --- Splunk/Collection.php | 13 +------------ Splunk/Endpoint.php | 24 +++++++++++++++++++++++- Splunk/Entity.php | 13 +------------ 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/Splunk/Collection.php b/Splunk/Collection.php index aa0341a..9f1c483 100644 --- a/Splunk/Collection.php +++ b/Splunk/Collection.php @@ -22,22 +22,11 @@ */ class Splunk_Collection extends Splunk_Endpoint { - private $loaded = FALSE; private $entries = NULL; // === Load === - /** Loads this collection if not already done. Returns self. */ - private function validate() - { - if (!$this->loaded) - { - $this->load(); - } - return $this; - } - - private function load() + protected function load() { $response = $this->service->get($this->path); $xml = new SimpleXMLElement($response->body); diff --git a/Splunk/Endpoint.php b/Splunk/Endpoint.php index 884ae37..82d97c2 100644 --- a/Splunk/Endpoint.php +++ b/Splunk/Endpoint.php @@ -20,14 +20,36 @@ * * @package Splunk */ -class Splunk_Endpoint +abstract class Splunk_Endpoint { protected $service; protected $path; + protected $loaded = FALSE; + public function __construct($service, $path) { $this->service = $service; $this->path = $path; } + + // === Load === + + /** Loads this resource if not already done. Returns self. */ + protected function validate() + { + if (!$this->loaded) + { + $this->load(); + assert($this->loaded); + } + return $this; + } + + /** + * Loads this resource. + * + * Implementations must set $this->loaded to TRUE before returning. + */ + protected abstract function load(); } \ No newline at end of file diff --git a/Splunk/Entity.php b/Splunk/Entity.php index 6fc9134..5337a89 100644 --- a/Splunk/Entity.php +++ b/Splunk/Entity.php @@ -22,7 +22,6 @@ */ class Splunk_Entity extends Splunk_Endpoint implements ArrayAccess { - private $loaded = FALSE; private $data; private $content; @@ -43,17 +42,7 @@ public function __construct($service, $path, $data=NULL) // === Load === - /** Loads this entity if not already done. Returns self. */ - private function validate() - { - if (!$this->loaded) - { - $this->load(); - } - return $this; - } - - private function load() + protected function load() { $response = $this->service->get($this->path); $xml = new SimpleXMLElement($response->body); From 0a7cdc3bc9b3de89182f7360c80f19939d6cf55c Mon Sep 17 00:00:00 2001 From: David Foster Date: Tue, 26 Jun 2012 15:13:46 -0700 Subject: [PATCH 10/12] Wordsmithing documentation. --- Splunk/Collection.php | 2 +- Splunk/Entity.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Splunk/Collection.php b/Splunk/Collection.php index 9f1c483..9156ca2 100644 --- a/Splunk/Collection.php +++ b/Splunk/Collection.php @@ -52,7 +52,7 @@ private function loadEntry($entryData) // === Children === /** - * Returns the unique entity with the specified name. + * Returns the unique entity with the specified name in this collection. * * @param string $name * @return Splunk_Entity diff --git a/Splunk/Entity.php b/Splunk/Entity.php index 5337a89..904a8e5 100644 --- a/Splunk/Entity.php +++ b/Splunk/Entity.php @@ -30,6 +30,7 @@ class Splunk_Entity extends Splunk_Endpoint implements ArrayAccess * @param string $path * @param SimpleXMLElement $data (optional) The XML of this entity, * as received from the REST API. + * If omitted, will be loaded on demand. */ public function __construct($service, $path, $data=NULL) { From d2012db7726376a6348f24ac88fe4c4180f897d6 Mon Sep 17 00:00:00 2001 From: David Foster Date: Tue, 26 Jun 2012 15:21:21 -0700 Subject: [PATCH 11/12] Document AtomFeed class. --- Splunk/AtomFeed.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Splunk/AtomFeed.php b/Splunk/AtomFeed.php index 0be7295..093b75c 100644 --- a/Splunk/AtomFeed.php +++ b/Splunk/AtomFeed.php @@ -16,6 +16,8 @@ */ /** + * Contains utilities for parsing Atom feeds received from the Splunk REST API. + * * @package Splunk */ class Splunk_AtomFeed @@ -23,6 +25,12 @@ class Splunk_AtomFeed /** Name of the 's' namespace in Splunk Atom feeds. */ const NS_S = 'http://dev.splunk.com/ns/rest'; + /** + * Parses and returns the value inside the specified XML element. + * + * @param SimpleXMLElement $containerXml + * @returns mixed + */ public static function parseValueInside($containerXml) { $dictValue = $containerXml->children(Splunk_AtomFeed::NS_S)->dict; From 25f89111cf3aee3e44199b13fa55f1c6f2a67e82 Mon Sep 17 00:00:00 2001 From: David Foster Date: Tue, 26 Jun 2012 16:11:37 -0700 Subject: [PATCH 12/12] Document how to generate a code coverage report. --- .gitignore | 3 +++ tests/README.md | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/.gitignore b/.gitignore index c099e65..8d5f59b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,8 @@ # Generated documentation docs/ +# Generated code coverage report +coverage/ + # Local settings *.local.php diff --git a/tests/README.md b/tests/README.md index 3123c62..680e0b7 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,9 +1,15 @@ ## Requirements * PHPUnit 3.6 +* Xdebug 2.2.0 (for code coverage) ## Running the Tests Navigate to the root directory containing `Splunk.php`, then run: phpunit tests + +To generate a code coverage report, run: + + phpunit --coverage-html coverage tests + open coverage/Splunk.html