Index: /sandbox/webservice/api/library/tonic/phpdoctor.ini
===================================================================
--- /sandbox/webservice/api/library/tonic/phpdoctor.ini	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/phpdoctor.ini	(revision 6019)
@@ -0,0 +1,14 @@
+; Tonic configuration file for PHPDoctor
+
+files = "*.php"
+ignore = ".git"
+doclet = standard
+default_package = "Tonic"
+overview = README.markdown
+
+d = "apidocs"
+windowtitle = "Tonic"
+doctitle = "Tonic: A RESTful Web App Development PHP Library"
+header = "Tonic"
+footer = "Tonic"
+
Index: /sandbox/webservice/api/library/tonic/LICENSE
===================================================================
--- /sandbox/webservice/api/library/tonic/LICENSE	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/LICENSE	(revision 6019)
@@ -0,0 +1,18 @@
+Copyright (c) 2009 Paul James
+ 
+Permission is hereby granted, free of charge, to any person obtaining a copy of 
+this software and associated documentation files (the "Software"), to deal in 
+the Software without restriction, including without limitation the rights to 
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 
+the Software, and to permit persons to whom the Software is furnished to do so, 
+subject to the following conditions:
+ 
+The above copyright notice and this permission notice shall be included in all 
+copies or substantial portions of the Software.
+ 
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Index: /sandbox/webservice/api/library/tonic/tests/response.php
===================================================================
--- /sandbox/webservice/api/library/tonic/tests/response.php	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/tests/response.php	(revision 6019)
@@ -0,0 +1,56 @@
+<?php
+
+require_once('def/response.php');
+
+/**
+ * @namespace Tonic\Tests
+ */
+class ResponseTester extends UnitTestCase {
+    
+    function testDefaultCacheHeader() {
+        
+        $config = array(
+            'uri' => '/responsetest'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        $response = $resource->exec($request);
+        $response->addCacheHeader();
+        
+        $this->assertEqual($response->headers['Cache-Control'], 'max-age=86400, must-revalidate');
+        
+    }
+    
+    function testNoCacheHeader() {
+        
+        $config = array(
+            'uri' => '/responsetest'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        $response = $resource->exec($request);
+        $response->addCacheHeader(0);
+        
+        $this->assertEqual($response->headers['Cache-Control'], 'no-cache');
+        
+    }
+    
+    function testAddEtag() {
+        
+        $config = array(
+            'uri' => '/responsetest'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        $response = $resource->exec($request);
+        $response->addEtag("123123");
+        
+        $this->assertEqual($response->headers['Etag'], '"123123"');
+        
+    }
+    
+}
+
Index: /sandbox/webservice/api/library/tonic/tests/test.php
===================================================================
--- /sandbox/webservice/api/library/tonic/tests/test.php	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/tests/test.php	(revision 6019)
@@ -0,0 +1,43 @@
+<?php
+
+error_reporting(error_reporting() & ~2048 & ~8192); // Make sure E_STRICT and E_DEPRECATED are disabled
+
+require_once('simpletest/unit_tester.php');
+require_once('simpletest/reporter.php');
+
+$core = new TestSuite('Core');
+
+require_once('../lib/tonic.php');
+
+$core->addFile('request.php');
+$core->addFile('resource.php');
+$core->addFile('response.php');
+$core->addFile('filesystem.php');
+$core->addFile('filesystemcollection.php');
+
+$test = new TestSuite('Tonic');
+$test->add($core);
+
+//*
+@include_once 'PHP/CodeCoverage.php';
+if (class_exists('PHP_CodeCoverage')) {
+    $coverage = new PHP_CodeCoverage;
+    $coverage->start('Tonic');
+}
+//*/
+
+if (TextReporter::inCli()) {
+	$test->run(new TextReporter());
+} else {
+    $test->run(new HtmlReporter());
+}
+
+if (isset($coverage)) {
+    $coverage->stop();
+    
+    require_once 'PHP/CodeCoverage/Report/HTML.php';
+    
+    $writer = new PHP_CodeCoverage_Report_HTML;
+    $writer->process($coverage, 'report');
+}
+
Index: /sandbox/webservice/api/library/tonic/tests/filesystemcollection.php
===================================================================
--- /sandbox/webservice/api/library/tonic/tests/filesystemcollection.php	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/tests/filesystemcollection.php	(revision 6019)
@@ -0,0 +1,137 @@
+<?php
+
+require_once('../lib/tonic.php');
+require_once('../examples/filesystem/filesystem.php');
+require_once('../examples/filesystem/filesystemcollection.php');
+
+/**
+ * @namespace Tonic\Tests
+ */
+class FilesystemCollectionTester extends UnitTestCase {
+    
+    var $testPath;
+    
+    function __construct() {
+        $this->testPath = sys_get_temp_dir();
+    }
+    
+    function setUp() {
+        @mkdir($this->testPath.DIRECTORY_SEPARATOR.'collection');
+        file_put_contents($this->testPath.DIRECTORY_SEPARATOR.'collectionIndex', '<ul>{{resources}}</ul>');
+        file_put_contents($this->testPath.DIRECTORY_SEPARATOR.'collection'.DIRECTORY_SEPARATOR.'1', 'one');
+        file_put_contents($this->testPath.DIRECTORY_SEPARATOR.'collection'.DIRECTORY_SEPARATOR.'2', 'two');
+    }
+    
+    function tearDown() {
+        unlink($this->testPath.DIRECTORY_SEPARATOR.'collectionIndex');
+        unlink($this->testPath.DIRECTORY_SEPARATOR.'collection'.DIRECTORY_SEPARATOR.'1');
+        unlink($this->testPath.DIRECTORY_SEPARATOR.'collection'.DIRECTORY_SEPARATOR.'2');
+        @unlink($this->testPath.DIRECTORY_SEPARATOR.'collection'.DIRECTORY_SEPARATOR.'3');
+        rmdir($this->testPath.DIRECTORY_SEPARATOR.'collection');
+    }
+    
+    function testCollection() {
+        
+        $config = array(
+            'uri' => '/filesystemcollection/collectionIndex'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        $response = $resource->exec($request);
+        
+        $this->assertEqual($response->code, 200);
+        $this->assertEqual($response->body, '<ul><li><a href="/filesystemcollection/collection/1">1</a></li><li><a href="/filesystemcollection/collection/2">2</a></li></ul>');
+        
+    }
+    
+    function testReadCollectionItem() {
+        
+        $config = array(
+            'uri' => '/filesystemcollection/collection/1'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        $response = $resource->exec($request);
+        
+        $this->assertEqual($response->code, 200);
+        $this->assertEqual($response->body, 'one');
+        
+    }
+    
+    function testCreateCollectionItem() {
+        
+        $config = array(
+            'uri' => '/filesystemcollection/collectionIndex',
+            'method' => 'POST'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        $response = $resource->exec($request);
+        
+        $this->assertEqual($response->code, 411);
+        
+        $config = array(
+            'uri' => '/filesystemcollection/collectionIndex',
+            'method' => 'POST',
+            'data' => 'three'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        $response = $resource->exec($request);
+        
+        $this->assertEqual($response->code, 201);
+        $this->assertEqual($response->headers['Location'], '/filesystemcollection/collection/3');
+        
+        $config = array(
+            'uri' => '/filesystemcollection/collectionIndex'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        $response = $resource->exec($request);
+        
+        $this->assertEqual($response->code, 200);
+        $this->assertEqual($response->body, '<ul><li><a href="/filesystemcollection/collection/1">1</a></li><li><a href="/filesystemcollection/collection/2">2</a></li><li><a href="/filesystemcollection/collection/3">3</a></li></ul>');
+        
+    }
+    
+}
+
+
+/* Test resource definitions */
+
+/**
+ * @namespace Tonic\Tests
+ * @uri /filesystemcollection/collection/.*
+ */
+class TestFileSystemCollectionItem extends FilesystemResource {
+    
+    function __construct() {
+        
+        $this->path = sys_get_temp_dir();
+        $this->uriStub = '/filesystemcollection';
+        
+    }
+    
+}
+
+/**
+ * @namespace Tonic\Tests
+ * @uri /filesystemcollection/collectionIndex
+ */
+class TestFileSystemCollection extends FilesystemCollection {
+    
+    function __construct() {
+        
+        $this->path = sys_get_temp_dir();
+        $this->uriStub = '/filesystemcollection';
+        $this->collection = sys_get_temp_dir().'/collection';
+        
+    }
+    
+}
+
Index: /sandbox/webservice/api/library/tonic/tests/filesystem.php
===================================================================
--- /sandbox/webservice/api/library/tonic/tests/filesystem.php	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/tests/filesystem.php	(revision 6019)
@@ -0,0 +1,149 @@
+<?php
+
+require_once('../lib/tonic.php');
+require_once('../examples/filesystem/filesystem.php');
+
+/**
+ * @namespace Tonic\Tests
+ */
+class FilesystemTester extends UnitTestCase {
+    
+    function testReadFile() {
+        
+        $testFilename = sys_get_temp_dir().DIRECTORY_SEPARATOR.'tonicFilesystemTest';
+        file_put_contents($testFilename, 'test');
+        
+        $config = array(
+            'uri' => '/filesystem/one/tonicFilesystemTest'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        $response = $resource->exec($request);
+        
+        $this->assertEqual($response->code, 200);
+        $this->assertEqual($response->body, 'test');
+        
+        unlink($testFilename);
+    }
+    
+    function testReadDefaultDocument() {
+        
+        $testFilename = sys_get_temp_dir().DIRECTORY_SEPARATOR.'default.html';
+        file_put_contents($testFilename, 'test');
+        
+        $config = array(
+            'uri' => '/filesystem/one/'
+        );
+        
+        $request = new Request($config);
+        
+        $resource = $request->loadResource();
+        $response = $resource->exec($request);
+        
+        $this->assertEqual($response->code, 200);
+        $this->assertEqual($response->body, 'test');
+        
+        unlink($testFilename);
+        
+    }
+    
+    function testReadNoResource() {
+        
+        $config = array(
+            'uri' => '/filesystem/one/tonicFilesystemTestDoesntExist'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        $response = $resource->exec($request);
+        
+        $this->assertEqual($response->code, 404);
+        
+    }
+    
+    function testCreateResource() {
+        
+        $testFilename = sys_get_temp_dir().DIRECTORY_SEPARATOR.'tonicFilesystemTest';
+        
+        $config = array(
+            'uri' => '/filesystem/one/tonicFilesystemTest',
+            'method' => 'PUT'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        $response = $resource->exec($request);
+        
+        $this->assertEqual($response->code, 411);
+        
+        $config['data'] = 'test';
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        $response = $resource->exec($request);
+        
+        $this->assertEqual($response->code, 204);
+        $this->assertTrue(file_exists($testFilename));
+        $this->assertEqual(file_get_contents($testFilename), 'test');
+        
+        unlink($testFilename);
+        
+    }
+    
+    function testDeleteResource() {
+        
+        $testFilename = sys_get_temp_dir().DIRECTORY_SEPARATOR.'tonicFilesystemTest';
+        file_put_contents($testFilename, 'test');
+        
+        $config = array(
+            'uri' => '/filesystem/one/tonicFilesystemTest',
+            'method' => 'DELETE'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        $response = $resource->exec($request);
+        
+        $this->assertEqual($response->code, 204);
+        $this->assertFalse(file_exists($testFilename));
+        
+    }
+    
+    function testDeleteNoResource() {
+        
+        $config = array(
+            'uri' => '/filesystem/one/tonicFilesystemTestDoesntExist',
+            'method' => 'DELETE'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        $response = $resource->exec($request);
+        
+        $this->assertEqual($response->code, 404);
+        
+    }
+    
+}
+
+
+/* Test resource definitions */
+
+/**
+ * @namespace Tonic\Tests
+ * @uri /filesystem/one
+ * @uri /filesystem/one/.*
+ */
+class TestFileSystem extends FilesystemResource {
+    
+    function __construct($parameters) {
+        
+        parent::__construct($parameters);
+        $this->path = sys_get_temp_dir().DIRECTORY_SEPARATOR;
+        $this->uriStub = '/filesystem/one/';
+        
+    }
+    
+}
+
Index: /sandbox/webservice/api/library/tonic/tests/resource.php
===================================================================
--- /sandbox/webservice/api/library/tonic/tests/resource.php	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/tests/resource.php	(revision 6019)
@@ -0,0 +1,80 @@
+<?php
+
+require_once('def/resource.php');
+
+/**
+ * @namespace Tonic\Tests
+ */
+class ResourceTester extends UnitTestCase {
+
+	function testRegexFirstResourceHaha() {
+		$config = array(
+			'uri' => '/hahaha/youfunny'
+		);
+
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        $response = $resource->exec($request);
+        
+        $this->assertEqual($response->code, '200');
+        $this->assertEqual($response->body, 'hahaha you funny');
+	}
+    
+	function testRegexFirstResourceHoho() {
+		$config = array(
+			'uri' => '/hohoho/youfunny'
+		);
+
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        $response = $resource->exec($request);
+        
+        $this->assertEqual($response->code, '200');
+        $this->assertEqual($response->body, 'hohoho you funny');
+	}
+    
+    function testFunctioningResourceExec() {
+        
+        $config = array(
+            'uri' => '/resourcetest/one'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        $response = $resource->exec($request);
+        
+        $this->assertEqual($response->code, '200');
+        $this->assertEqual($response->body, 'test');
+        
+    }
+    
+    function testNoParametersArgumentToResourceConstructor() {
+        
+        $config = array(
+            'uri' => '/resourcetest/badconstructor'
+        );
+        
+        $this->expectError(new PatternExpectation('/Missing argument 1 for Resource::__construct/'));
+        $this->expectError(new PatternExpectation('/Undefined variable: parameters/'));
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        
+    }
+    
+    function testMethodDoesNotReturnResponseObject() {
+        
+        $config = array(
+            'uri' => '/resourcetest/badmethodresponse'
+        );
+        
+        $this->expectException(new PatternExpectation('/Method GET of (.+\\\\)?TestBadMethodResponse did not return a Response object/'));
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        $response = $resource->exec($request);
+        
+    }
+
+}
+
Index: /sandbox/webservice/api/library/tonic/tests/def/response.php
===================================================================
--- /sandbox/webservice/api/library/tonic/tests/def/response.php	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/tests/def/response.php	(revision 6019)
@@ -0,0 +1,20 @@
+<?php
+
+/* Test resource definitions */
+
+/**
+ * @namespace Tonic\Tests
+ * @uri /responsetest
+ */
+class TestResponse extends Resource {
+    
+    function get($request) {
+        
+        $response = new Response($request);
+        $response->body = 'test';
+        return $response;
+        
+    }
+    
+}
+
Index: /sandbox/webservice/api/library/tonic/tests/def/resource.php
===================================================================
--- /sandbox/webservice/api/library/tonic/tests/def/resource.php	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/tests/def/resource.php	(revision 6019)
@@ -0,0 +1,59 @@
+<?php
+
+/* Test resource definitions */
+
+/**
+ * @namespace Tonic\Tests
+ * @uri /([hoa]+)/youfunny
+ */
+class TestRegexFirstResource extends Resource {
+	function get($request, $haha) {
+		$response = new Response($request);
+		$response->body = "$haha you funny";
+		return $response;
+	}
+}
+
+/**
+ * @namespace Tonic\Tests
+ * @uri /resourcetest/one
+ */
+class TestResource extends Resource {
+    
+    function get($request) {
+        
+        $response = new Response($request);
+        $response->body = 'test';
+        return $response;
+        
+    }
+    
+}
+
+/**
+ * @namespace Tonic\Tests
+ * @uri /resourcetest/badconstructor
+ */
+class TestBadResourceConstructor extends Resource {
+    
+    function __construct() {
+        
+        parent::__construct();
+        
+    }
+    
+}
+
+/**
+ * @namespace Tonic\Tests
+ * @uri /resourcetest/badmethodresponse
+ */
+class TestBadMethodResponse extends Resource {
+    
+    function get($request) {
+        
+        return TRUE;
+        
+    }
+    
+}
Index: /sandbox/webservice/api/library/tonic/tests/def/Autoload.php
===================================================================
--- /sandbox/webservice/api/library/tonic/tests/def/Autoload.php	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/tests/def/Autoload.php	(revision 6019)
@@ -0,0 +1,8 @@
+<?php
+
+/**
+ * @namespace Tonic\Tests
+ */
+class Autoload extends Resource {
+
+}
Index: /sandbox/webservice/api/library/tonic/tests/def/request.php
===================================================================
--- /sandbox/webservice/api/library/tonic/tests/def/request.php	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/tests/def/request.php	(revision 6019)
@@ -0,0 +1,135 @@
+<?php
+
+/* Test resource definitions */
+
+/**
+ * @namespace Tonic\Tests
+ * @uri /requesttest/one
+ * @uri /requesttest/three/(.+)/four 12
+ */
+class NewResource extends Resource {
+
+}
+
+/**
+ * @namespace Tonic\Tests
+ * @uri /requesttest/one/two
+ */
+class ChildResource extends NewResource {
+
+}
+
+/**
+ * @namespace Tonic\Tests
+ * @uri /requesttest/railsstyle/:param/:param2
+ * @uri /requesttest/uritemplatestyle/{param}/{param2}
+ */
+class TwoUriParams extends Resource {
+
+    var $receivedParams;
+    
+    function get($request, $param, $param2) {
+        $this->receivedParams = array(
+            'param' => $param,
+            'param2' => $param2
+        );
+        return new Response($request);
+    }
+    
+}
+
+/**
+ * @namespace Tonic\Tests
+ * @uri /requesttest/railsmixedstyle/{param}/(.+)/{param2}/(.+)
+ * @uri /requesttest/uritemplatemixedstyle/{param}/(.+)/{param2}/(.+)
+ * @uri /requesttest/mixedstyle/:param/(.+)/{param2}/(.+)
+ */
+class FourUriParams extends Resource {
+    
+    var $receivedParams;
+    
+    function get($request, $something, $otherthing, $param, $param2) {
+        $this->receivedParams = array(
+            'param' => $param,
+            'param2' => $param2,
+            'something' => $something,
+            'otherthing' => $otherthing
+        );
+        return new Response($request);
+    }
+    
+}
+
+/**
+ * @namespace Tonic\Tests
+ * @uri /requesttest/trailingslashurl/
+ */
+class TrailingSlashUrl extends Resource {
+
+}
+
+/**
+ * @namespace Tonic\Tests
+ * @uri /requesttest/optional(?:/param1/([^/]*))?
+ * @uri /requesttest/optional(?:/([a-z]+))?
+ */
+class OptionalParams extends Resource {
+    
+}
+
+/**
+ * @namespace Tonic\Tests
+ * @uri /requesttest/squiggly/(\d+-[0-9a-f]{8}-[0-9a-f]{6})
+ * @uri /requesttest/noncapture/(?:something)?
+ */
+class SquigglyRegexResource extends Resource {
+
+}
+
+/**
+ * @namespace Tonic\Tests
+ * @uri /requesttest/httpmethods
+ */
+class MethodTestResource extends Resource {
+    
+    function options($request) {
+        return new Response($request);
+    }
+    
+    function woot($request) {
+        return new Response($request);
+    }
+    
+}
+
+/**
+ * @namespace Tonic\Tests
+ * @uri /requesttest/priority 1
+  */
+class PriorityTestLessImportantResource extends Resource {
+
+}
+
+/**
+ * @namespace Tonic\Tests
+ * @uri /requesttest/priority 2
+  */
+class PriorityTestMoreImportantResource extends Resource {
+
+}
+
+/**
+ * @namespace Tonic\Tests
+ * @uri /requesttest/differenturi(priority) 1
+  */
+class PriorityTestDifferntURILessImportantResource extends Resource {
+
+}
+
+/**
+ * @namespace Tonic\Tests
+ * @uri /requesttest/differenturipriority 2
+  */
+class PriorityTestDifferntURIMoreImportantResource extends Resource {
+
+}
Index: /sandbox/webservice/api/library/tonic/tests/request.php
===================================================================
--- /sandbox/webservice/api/library/tonic/tests/request.php	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/tests/request.php	(revision 6019)
@@ -0,0 +1,826 @@
+<?php
+
+require_once('def/request.php');
+
+/**
+ * @namespace Tonic\Tests
+ */
+class RequestTester extends UnitTestCase {
+    
+    function testRequestURI() {
+        
+        $config = array(
+            'uri' => '/requesttest/one/two'
+        );
+        $request = new Request($config);
+        $this->assertEqual($request->uri, $config['uri']);
+        
+        $_SERVER['REQUEST_URI'] = '/requesttest/one/two';
+        $request = new Request();
+        $this->assertEqual($request->uri, $config['uri']);
+        
+    }
+    
+    function testRequestBaseUri() {
+        
+        $config = array(
+            'baseUri' => '/some/sub/dir'
+        );
+        
+        $request = new Request($config);
+        
+        $this->assertEqual($request->baseUri, $config['baseUri']);
+        
+    }
+    
+    function testAssignMimetypes() {
+        
+        $config = array(
+            'mimetypes' => array(
+                'some' => 'text/something'
+            )
+        );
+        $request = new Request($config);
+        $this->assertEqual($request->mimetypes['some'], 'text/something');
+        
+    }
+    
+    function testGetRequestMethod() {
+        
+        $config = array();
+        
+        $request = new Request($config);
+        
+        $this->assertEqual($request->method, 'GET');
+        
+    }
+    
+    function testPostRequestMethodWithData() {
+        
+        $config = array(
+            'method' => 'post',
+            'data' => 'some data'
+        );
+        
+        $request = new Request($config);
+        
+        $this->assertEqual($request->method, 'POST');
+        $this->assertEqual($request->data, 'some data');
+        
+    }
+    
+    function testPutRequestMethodWithData() {
+        
+        $config = array(
+            'method' => 'put',
+            'data' => 'some data'
+        );
+        
+        $request = new Request($config);
+        
+        $this->assertEqual($request->method, 'PUT');
+        $this->assertEqual($request->data, 'some data');
+        
+    }
+    
+    function testConnegOnBareURI() {
+        
+        $config = array(
+            'uri' => '/requesttest/one/two',
+            'accept' => '',
+            'acceptLang' => ''
+        );
+        
+        $request = new Request($config);
+        $this->assertEqual($request->negotiatedUris, array(
+            '/requesttest/one/two'
+        ));
+        $this->assertEqual($request->formatNegotiatedUris, array(
+            '/requesttest/one/two'
+        ));
+        $this->assertEqual($request->languageNegotiatedUris, array(
+            '/requesttest/one/two'
+        ));
+        $this->assertEqual($request->uri, '/requesttest/one/two');
+    
+    }
+    
+    function testConnegOnExtensionURI() {
+        
+        $config = array(
+            'uri' => '/requesttest/one/two.html',
+            'accept' => '',
+            'acceptLang' => ''
+        );
+        
+        $request = new Request($config);
+        $this->assertEqual($request->negotiatedUris, array(
+            '/requesttest/one/two.html',
+            '/requesttest/one/two'
+        ));
+        $this->assertEqual($request->formatNegotiatedUris, array(
+            '/requesttest/one/two.html',
+            '/requesttest/one/two'
+        ));
+        $this->assertEqual($request->languageNegotiatedUris, array(
+            '/requesttest/one/two.html',
+            '/requesttest/one/two'
+        ));
+        $this->assertEqual($request->uri, '/requesttest/one/two');
+        
+    }
+    
+    function testConnegOnBareURIWithAccept() {
+        
+        $config = array(
+            'uri' => '/requesttest/one/two',
+            'accept' => 'image/png;q=0.5,text/html',
+            'acceptLang' => ''
+        );
+        
+        $request = new Request($config);
+        $this->assertEqual($request->negotiatedUris, array(
+            '/requesttest/one/two.html',
+            '/requesttest/one/two.png',
+            '/requesttest/one/two'
+        ));
+        $this->assertEqual($request->formatNegotiatedUris, array(
+            '/requesttest/one/two.html',
+            '/requesttest/one/two.png',
+            '/requesttest/one/two'
+        ));
+        $this->assertEqual($request->languageNegotiatedUris, array(
+            '/requesttest/one/two'
+        ));
+        
+    }
+    
+    function testConnegOnExtensionURIWithAccept() {
+        
+        $config = array(
+            'uri' => '/requesttest/one/two.html',
+            'accept' => 'image/png;q=0.5,text/html',
+            'acceptLang' => ''
+        );
+        
+        $request = new Request($config);
+        $this->assertEqual($request->negotiatedUris, array(
+            '/requesttest/one/two.html',
+            '/requesttest/one/two.png.html',
+            '/requesttest/one/two.png',
+            '/requesttest/one/two'
+        ));
+        $this->assertEqual($request->formatNegotiatedUris, array(
+            '/requesttest/one/two.html',
+            '/requesttest/one/two.png',
+            '/requesttest/one/two'
+        ));
+        $this->assertEqual($request->languageNegotiatedUris, array(
+            '/requesttest/one/two.html',
+            '/requesttest/one/two'
+        ));
+        
+    }
+    
+    function testConnegOnBareURIWithAcceptLang() {
+        
+        $config = array(
+            'uri' => '/requesttest/one/two',
+            'accept' => '',
+            'acceptLang' => 'fr;q=0.5,en'
+        );
+        
+        $request = new Request($config);
+        $this->assertEqual($request->negotiatedUris, array(
+            '/requesttest/one/two.en',
+            '/requesttest/one/two.fr',
+            '/requesttest/one/two'
+        ));
+        $this->assertEqual($request->formatNegotiatedUris, array(
+            '/requesttest/one/two'
+        ));
+        $this->assertEqual($request->languageNegotiatedUris, array(
+            '/requesttest/one/two.en',
+            '/requesttest/one/two.fr',
+            '/requesttest/one/two'
+        ));
+        
+    }
+    
+    function testConnegOnExtensionURIWithAcceptLang() {
+        
+        $config = array(
+            'uri' => '/requesttest/one/two.html',
+            'accept' => '',
+            'acceptLang' => 'fr;q=0.5,en'
+        );
+        
+        $request = new Request($config);
+        $this->assertEqual($request->negotiatedUris, array(
+            '/requesttest/one/two.html.en',
+            '/requesttest/one/two.html.fr',
+            '/requesttest/one/two.html',
+            '/requesttest/one/two.en',
+            '/requesttest/one/two.fr',
+            '/requesttest/one/two'
+        ));
+        $this->assertEqual($request->formatNegotiatedUris, array(
+            '/requesttest/one/two.html',
+            '/requesttest/one/two'
+        ));
+        $this->assertEqual($request->languageNegotiatedUris, array(
+            '/requesttest/one/two.html',
+            '/requesttest/one/two.en',
+            '/requesttest/one/two.fr',
+            '/requesttest/one/two'
+        ));
+        
+    }
+    
+    function testConnegOnBareURIWithAcceptAndAcceptLang() {
+        
+        $config = array(
+            'uri' => '/requesttest/one/two',
+            'accept' => 'image/png;q=0.5,text/html',
+            'acceptLang' => 'fr;q=0.5,en'
+        );
+        
+        $request = new Request($config);
+        $this->assertEqual($request->negotiatedUris, array(
+            '/requesttest/one/two.html.en',
+            '/requesttest/one/two.html.fr',
+            '/requesttest/one/two.html',
+            '/requesttest/one/two.png.en',
+            '/requesttest/one/two.png.fr',
+            '/requesttest/one/two.png',
+            '/requesttest/one/two.en',
+            '/requesttest/one/two.fr',
+            '/requesttest/one/two'
+        ));
+        $this->assertEqual($request->formatNegotiatedUris, array(
+            '/requesttest/one/two.html',
+            '/requesttest/one/two.png',
+            '/requesttest/one/two'
+        ));
+        $this->assertEqual($request->languageNegotiatedUris, array(
+            '/requesttest/one/two.en',
+            '/requesttest/one/two.fr',
+            '/requesttest/one/two'
+        ));
+        
+    }
+    
+    function testConnegOnExtensionURIWithAcceptAndAcceptLang() {
+        
+        $config = array(
+            'uri' => '/requesttest/one/two.html',
+            'accept' => 'image/png;q=0.5,text/html',
+            'acceptLang' => 'fr;q=0.5,en'
+        );
+        
+        $request = new Request($config);
+        $this->assertEqual($request->negotiatedUris, array(
+            '/requesttest/one/two.html.en',
+            '/requesttest/one/two.html.fr',
+            '/requesttest/one/two.html',
+            '/requesttest/one/two.png.html',
+            '/requesttest/one/two.png.en',
+            '/requesttest/one/two.png.fr',
+            '/requesttest/one/two.png',
+            '/requesttest/one/two.en',
+            '/requesttest/one/two.fr',
+            '/requesttest/one/two'
+        ));
+        $this->assertEqual($request->formatNegotiatedUris, array(
+            '/requesttest/one/two.html',
+            '/requesttest/one/two.png',
+            '/requesttest/one/two'
+        ));
+        $this->assertEqual($request->languageNegotiatedUris, array(
+            '/requesttest/one/two.html',
+            '/requesttest/one/two.en',
+            '/requesttest/one/two.fr',
+            '/requesttest/one/two'
+        ));
+        
+    }
+    
+    function testResourceLoaderWithNoResources() {
+        
+        $config = array(
+            'uri' => '/three'
+        );
+        
+        $request = new Request($config);
+        
+        try {
+            $resource = $request->loadResource();
+            $this->fail('Expected ResponseException to be thrown');
+        } catch (ResponseException $e) {
+            $this->assertEqual($e->getCode(), 404);
+        }
+        
+    }
+    
+    function testResourceLoaderWithAResources() {
+        
+        $config = array(
+            'uri' => '/requesttest/one'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        
+        $this->assertPattern('/NewResource/', get_class($resource));
+        
+    }
+    
+    function testResourceLoaderWithAChildResources() {
+        
+        $config = array(
+            'uri' => '/requesttest/one/two'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        
+        $this->assertPattern('/ChildResource/', get_class($resource));
+        
+    }
+    
+    function testResourceLoaderWithRegexURIMatch() {
+        
+        $config = array(
+            'uri' => '/requesttest/three/something/four'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        
+        $this->assertPattern('/NewResource/', get_class($resource));
+        $this->assertPattern('/0: something/', $resource);
+        
+    }
+    
+    function testIfMatch() {
+        
+        $config = array(
+            'uri' => '/requesttest/one',
+            'ifMatch' => '123123'
+        );
+        $request = new Request($config);
+        $this->assertEqual($request->ifMatch, array('123123'));
+        
+        $config = array(
+            'uri' => '/requesttest/one',
+            'ifMatch' => '"123123"'
+        );
+        $request = new Request($config);
+        $this->assertEqual($request->ifMatch, array('123123'));
+        
+        $config = array(
+            'uri' => '/requesttest/one',
+            'ifMatch' => '"123123","456456"'
+        );
+        $request = new Request($config);
+        $this->assertEqual($request->ifMatch, array('123123', '456456'));
+        
+        $config = array(
+            'uri' => '/requesttest/one',
+            'ifMatch' => '"123123", "456456"'
+        );
+        $request = new Request($config);
+        $this->assertEqual($request->ifMatch, array('123123', '456456'));
+        $this->assertTrue($request->ifMatch('123123'));
+        $this->assertFalse($request->ifMatch('123456'));
+        
+        $config = array(
+            'uri' => '/requesttest/one',
+            'ifMatch' => '*'
+        );
+        $request = new Request($config);
+        $this->assertEqual($request->ifMatch, array('*'));
+        $this->assertTrue($request->ifMatch('123123'));
+        $this->assertTrue($request->ifMatch('123456'));
+        
+    }
+    
+    function testIfNoneMatch() {
+        
+        $config = array(
+            'uri' => '/requesttest/one',
+            'ifNoneMatch' => '123123'
+        );
+        $request = new Request($config);
+        $this->assertEqual($request->ifNoneMatch, array('123123'));
+        
+        $config = array(
+            'uri' => '/requesttest/one',
+            'ifNoneMatch' => '"123123"'
+        );
+        $request = new Request($config);
+        $this->assertEqual($request->ifNoneMatch, array('123123'));
+        
+        $config = array(
+            'uri' => '/requesttest/one',
+            'ifNoneMatch' => '"123123","456456"'
+        );
+        $request = new Request($config);
+        $this->assertEqual($request->ifNoneMatch, array('123123', '456456'));
+        
+        $config = array(
+            'uri' => '/requesttest/one',
+            'ifNoneMatch' => '123123, 456456'
+        );
+        $request = new Request($config);
+        $this->assertEqual($request->ifNoneMatch, array('123123', '456456'));
+        $this->assertTrue($request->ifNoneMatch('123123'));
+        $this->assertFalse($request->ifNoneMatch('123456'));
+        
+        $config = array(
+            'uri' => '/requesttest/one',
+            'ifNoneMatch' => '*'
+        );
+        $request = new Request($config);
+        $this->assertEqual($request->ifNoneMatch, array('*'));
+        $this->assertFalse($request->ifNoneMatch('123123'));
+        $this->assertFalse($request->ifNoneMatch('123456'));
+        
+        $config = array(
+            'uri' => '/requesttest/one',
+            'ifNoneMatch' => '123123, 456456',
+            'ifMatch' => '*'
+        );
+        $request = new Request($config);
+        $this->assertEqual($request->ifMatch, array('*'));
+        $this->assertTrue($request->ifMatch('123123'));
+        $this->assertTrue($request->ifMatch('123456'));
+        $this->assertFalse($request->ifNoneMatch('123123'));
+        $this->assertFalse($request->ifNoneMatch('123456'));
+        
+    }
+    
+    function test404NotFound() {
+        
+        $config = array(
+            'uri' => '/requesttest/notfound'
+        );
+        $request = new Request($config);
+        
+        try {
+            $resource = $request->loadResource();
+            $this->fail('Expected ResponseException to be thrown');
+        } catch (ResponseException $e) {
+            $this->assertEqual($e->getCode(), 404);
+        }
+        
+    }
+    
+    function testResourceDataLoading() {
+        
+        $config = array(
+            'uri' => '/requesttest/one/two'
+        );
+        
+        $request = new Request($config);
+        
+        $this->assertEqual($request->resources['/requesttest/one/two']['namespace'], 'Tonic\Tests');
+        $this->assertPattern('/ChildResource/', $request->resources['/requesttest/one/two']['class']);
+        $this->assertPattern('|def/request\.php|', $request->resources['/requesttest/one/two']['filename']);
+        $this->assertEqual($request->resources['/requesttest/one/two']['priority'], 0);
+        
+    }
+    
+    function testNamespaceMounting() {
+        
+        $config = array(
+            'uri' => '/foo/bar/requesttest/one',
+            'mount' => array(
+                'Tonic\Tests' => '/foo/bar'
+            )
+        );
+        
+        $request = new Request($config);
+        
+        $resource = $request->loadResource();
+        
+        $this->assertPattern('/NewResource/', get_class($resource));
+        
+    }
+    
+    function testURITemplateStyleUriParameters() {
+        
+        $config = array(
+            'uri' => '/requesttest/uritemplatestyle/woo/yay'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        
+        $this->assertPattern('/TwoUriParams/', get_class($resource));
+        $this->assertPattern('/param: woo/', $resource);
+        $this->assertPattern('/param2: yay/', $resource);
+        
+        $resource->exec($request);
+        $this->assertEqual($resource->receivedParams['param'], 'woo');
+        $this->assertEqual($resource->receivedParams['param2'], 'yay');
+        
+        
+        $config = array(
+            'uri' => '/requesttest/uritemplatemixedstyle/woo/yay/foo/bar'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        
+        $this->assertPattern('/FourUriParams/', get_class($resource));
+        $this->assertPattern('/param: woo/', $resource);
+        $this->assertPattern('/1: yay/', $resource);
+        $this->assertPattern('/param2: foo/', $resource);
+        $this->assertPattern('/3: bar/', $resource);
+        
+        $resource->exec($request);
+        $this->assertEqual($resource->receivedParams['param'], 'woo');
+        $this->assertEqual($resource->receivedParams['something'], 'yay');
+        $this->assertEqual($resource->receivedParams['param2'], 'foo');
+        $this->assertEqual($resource->receivedParams['otherthing'], 'bar');
+    }
+    
+    function testRailsStyleUriParameters() {
+        
+        $config = array(
+            'uri' => '/requesttest/railsstyle/woo/yay'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        
+        $this->assertPattern('/TwoUriParams/', get_class($resource));
+        $this->assertPattern('/param: woo/', $resource);
+        $this->assertPattern('/param2: yay/', $resource);
+        
+        $resource->exec($request);
+        $this->assertEqual($resource->receivedParams['param'], 'woo');
+        $this->assertEqual($resource->receivedParams['param2'], 'yay');
+        
+        
+        $config = array(
+            'uri' => '/requesttest/railsmixedstyle/woo/yay/foo/bar'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        
+        $this->assertPattern('/FourUriParams/', get_class($resource));
+        $this->assertPattern('/param: woo/', $resource);
+        $this->assertPattern('/1: yay/', $resource);
+        $this->assertPattern('/param2: foo/', $resource);
+        $this->assertPattern('/3: bar/', $resource);
+        
+        $resource->exec($request);
+        $this->assertEqual($resource->receivedParams['param'], 'woo');
+        $this->assertEqual($resource->receivedParams['something'], 'yay');
+        $this->assertEqual($resource->receivedParams['param2'], 'foo');
+        $this->assertEqual($resource->receivedParams['otherthing'], 'bar');
+    }
+    
+    function testMixedStyleUriParameters() {
+        
+        $config = array(
+            'uri' => '/requesttest/mixedstyle/woo/yay/foo/bar'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        
+        $this->assertPattern('/FourUriParams/', get_class($resource));
+        $this->assertPattern('/param: woo/', $resource);
+        $this->assertPattern('/1: yay/', $resource);
+        $this->assertPattern('/param2: foo/', $resource);
+        $this->assertPattern('/3: bar/', $resource);
+        
+        $resource->exec($request);
+        $this->assertEqual($resource->receivedParams['param'], 'woo');
+        $this->assertEqual($resource->receivedParams['something'], 'yay');
+        $this->assertEqual($resource->receivedParams['param2'], 'foo');
+        $this->assertEqual($resource->receivedParams['otherthing'], 'bar');
+    }
+    
+    function testAutoloadingOfResourcesWithoutAutoloadFunction() {
+        
+        $config = array(
+            'uri' => '/requesttest/autoload',
+            'autoload' => array(
+                '/requesttest/autoload' => 'Autoload'
+            )
+        );
+        
+        $request = new Request($config);
+        
+        $this->expectException(new PatternExpectation('/Unable to load resource/'));
+        $resource = $request->loadResource();
+        
+    }
+    
+    function testAutoloadingOfResources() {
+        
+        $config = array(
+            'uri' => '/requesttest/autoload',
+            'autoload' => array(
+                '/requesttest/autoload' => 'Autoload'
+            )
+        );
+        
+        function __autoload($className) {
+            if (file_exists('def/'.$className.'.php')) {
+                require_once 'def/'.$className.'.php';
+            }
+        }
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        
+        $this->assertPattern('/Autoload/', get_class($resource));
+        $this->assertPattern('/Class: Autoload/', $request);
+        $this->assertPattern('|File: '.dirname(__FILE__).DIRECTORY_SEPARATOR.'def/Autoload.php|', $request);
+        
+    }
+    
+    function testTrailingSlashRemoval() {
+        
+        $config = array(
+            'uri' => '/requesttest/trailingslashurl/'
+        );
+        $request = new Request($config);
+        $this->assertEqual($request->uri, substr($config['uri'], 0, -1));
+        
+    }
+    
+    function testRequestRootURI() {
+        
+        $config = array(
+            'uri' => '/'
+        );
+        $request = new Request($config);
+        $this->assertEqual($request->uri, $config['uri']);
+        
+        $_SERVER['REQUEST_URI'] = '/';
+        $request = new Request();
+        $this->assertEqual($request->uri, $config['uri']);
+        
+    }
+    
+    function testMethodNotAllowed() {
+        
+        $config = array(
+            'uri' => '/requesttest/one',
+            'method' => 'PUT'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        
+        try {
+            $response = $resource->exec($request);
+            $this->fail('Expected ResponseException to be thrown');
+        } catch (ResponseException $e) {
+            $this->assertEqual($e->getCode(), 405);
+        }
+        
+    }
+    
+    function testOptionalRegexURIParams() {
+        
+        $config = array(
+            'uri' => '/requesttest/optional'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        
+        $this->assertPattern('/OptionalParams/', get_class($resource));
+        $this->assertNoPattern('/0: /', $resource);
+        
+        $config = array(
+            'uri' => '/requesttest/optional/param1/woo'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        
+        $this->assertPattern('/OptionalParams/', get_class($resource));
+        $this->assertPattern('/0: woo/', $resource);
+        
+        $config = array(
+            'uri' => '/requesttest/optional/something'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        
+        $this->assertPattern('/OptionalParams/', get_class($resource));
+        $this->assertPattern('/0: something/', $resource);
+        
+    }
+    
+    function testResourceLoaderWithSquigglyBraceRegexURIMatch() {
+        
+        $config = array(
+            'uri' => '/requesttest/squiggly/9999-aaaaaaaa-bbbbbb'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        
+        $this->assertPattern('/SquigglyRegexResource/', get_class($resource));
+        
+        $config = array(
+            'uri' => '/requesttest/squiggly/9999-aaa-bbb'
+        );
+        
+        $request = new Request($config);
+        
+        try {
+            $resource = $request->loadResource();
+            $this->fail('Expected ResponseException to be thrown');
+        } catch (ResponseException $e) {
+            $this->assertTrue(TRUE);
+        }
+        
+    }
+    
+    function testResourceLoaderWithNonCapturingGroupRegexURIMatch() {
+        
+        $config = array(
+            'uri' => '/requesttest/noncapture/something'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        
+        $this->assertPattern('/SquigglyRegexResource/', get_class($resource));
+        
+    }
+    
+    function testAllowedHTTPMethods() {
+        
+        $config = array(
+            'uri' => '/requesttest/httpmethods',
+            'method' => 'OPTIONS'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        
+        try {
+            $response = $resource->exec($request);
+            $this->assertTrue(TRUE);
+        } catch (ResponseException $e) {
+            $this->fail('Did not expect ResponseException to be thrown');
+        }
+        
+        $config = array(
+            'uri' => '/requesttest/httpmethods',
+            'method' => 'WOOT'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        
+        try {
+            $response = $resource->exec($request);
+            $this->fail('Expected ResponseException to be thrown');
+        } catch (ResponseException $e) {
+            $this->assertTrue(TRUE);
+        }
+        
+    }
+    
+    function testURIPriority() {
+        
+        // with same @uri
+        
+        $config = array(
+            'uri' => '/requesttest/priority'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        
+        $this->assertEqual(get_class($resource), 'PriorityTestMoreImportantResource');
+        
+        // with different @uri
+        
+        $config = array(
+            'uri' => '/requesttest/differenturipriority'
+        );
+        
+        $request = new Request($config);
+        $resource = $request->loadResource();
+        
+        $this->assertEqual(get_class($resource), 'PriorityTestDifferntURIMoreImportantResource');
+        
+    }
+    
+}
+
Index: /sandbox/webservice/api/library/tonic/.travis.yml
===================================================================
--- /sandbox/webservice/api/library/tonic/.travis.yml	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/.travis.yml	(revision 6019)
@@ -0,0 +1,6 @@
+language: php
+php:
+  - 5.3
+  - 5.4
+before_install: wget https://github.com/downloads/Behat/Behat/behat.phar
+script: php behat.phar
Index: /sandbox/webservice/api/library/tonic/lib/tonic.php
===================================================================
--- /sandbox/webservice/api/library/tonic/lib/tonic.php	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/lib/tonic.php	(revision 6019)
@@ -0,0 +1,843 @@
+<?php
+/*
+ * This file is part of the Tonic.
+ * (c) Paul James <paul@peej.co.uk>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/* Turn on namespace in PHP5.3 if you have namespace collisions from the Tonic classnames
+namespace Tonic;
+use \ReflectionClass as ReflectionClass;
+use \ReflectionMethod as ReflectionMethod;
+use \Exception as Exception;
+//*/
+
+/**
+ * Model the data of the incoming HTTP request
+ * @namespace Tonic\Lib
+ */
+class Request {
+    
+    /**
+     * The requested URI
+     * @var str
+     */
+    public $uri;
+    
+    /**
+     * The URI where the front controller is positioned in the server URI-space
+     * @var str
+     */
+    public $baseUri = '';
+    
+    /**
+     * Array of possible URIs based upon accept and accept-language request headers in order of preference
+     * @var str[]
+     */
+    public $negotiatedUris = array();
+    
+    /**
+     * Array of possible URIs based upon accept request headers in order of preference
+     * @var str[]
+     */
+    public $formatNegotiatedUris = array();
+    
+    /**
+     * Array of possible URIs based upon accept-language request headers in order of preference
+     * @var str[]
+     */
+    public $languageNegotiatedUris = array();
+    
+    /**
+     * Array of accept headers in order of preference
+     * @var str[][]
+     */
+    public $accept = array();
+    
+    /**
+     * Array of accept-language headers in order of preference
+     * @var str[][]
+     */
+    public $acceptLang = array();
+    
+    /**
+     * Array of accept-encoding headers in order of preference
+     * @var str[]
+     */
+    public $acceptEncoding = array();
+    
+    /**
+     * Map of file/URI extensions to mimetypes
+     * @var str[]
+     */
+    public $mimetypes = array(
+        'html' => 'text/html',
+        'txt' => 'text/plain',
+        'php' => 'application/php',
+        'css' => 'text/css',
+        'js' => 'application/javascript',
+        'json' => 'application/json',
+        'xml' => 'application/xml',
+        'rss' => 'application/rss+xml',
+        'atom' => 'application/atom+xml',
+        'gz' => 'application/x-gzip',
+        'tar' => 'application/x-tar',
+        'zip' => 'application/zip',
+        'gif' => 'image/gif',
+        'png' => 'image/png',
+        'jpg' => 'image/jpeg',
+        'ico' => 'image/x-icon',
+        'swf' => 'application/x-shockwave-flash',
+        'flv' => 'video/x-flv',
+        'avi' => 'video/mpeg',
+        'mpeg' => 'video/mpeg',
+        'mpg' => 'video/mpeg',
+        'mov' => 'video/quicktime',
+        'mp3' => 'audio/mpeg'
+    );
+    
+    /**
+     * Supported HTTP methods
+     * @var str[]
+     */
+    public $HTTPMethods = array(
+        'GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS'
+    );
+    
+    /**
+     * Allowed resource methods
+     * @var str[]
+     */
+    public $allowedMethods = array();
+    
+    /**
+     * HTTP request method of incoming request
+     * @var str
+     */
+    public $method = 'GET';
+    
+    /**
+     * Body data of incoming request
+     * @var str
+     */
+    public $data;
+    
+    /**
+     * Array of if-match etags
+     * @var str[]
+     */
+    public $ifMatch = array();
+    
+    /**
+     * Array of if-none-match etags
+     * @var str[]
+     */
+    public $ifNoneMatch = array();
+    
+    /**
+     * The resource classes loaded and how they are wired to URIs
+     * @var str[]
+     */
+    public $resources = array();
+    
+    /**
+     * A list of URL to namespace/package mappings for routing requests to a
+     * group of resources that are wired into a different URL-space
+     * @var str[]
+     */
+    public $mounts = array();
+    
+    /**
+     * Set a default configuration option
+     */
+    protected function getConfig($config, $configVar, $serverVar = NULL, $default = NULL) {
+        if (isset($config[$configVar])) {
+            return $config[$configVar];
+        } elseif (isset($_SERVER[$serverVar]) && $_SERVER[$serverVar] != '') {
+            return $_SERVER[$serverVar];
+        } else {
+            return $default;
+        }
+    }
+    
+    /**
+     * Create a request object using the given configuration options.
+     *
+     * The configuration options array can contain the following:
+     *
+     * <dl>
+     * <dt>uri</dt> <dd>The URI of the request</dd>
+     * <dt>method</dt> <dd>The HTTP method of the request</dd>
+     * <dt>data</dt> <dd>The body data of the request</dd>
+     * <dt>accept</dt> <dd>An accept header</dd>
+     * <dt>acceptLang</dt> <dd>An accept-language header</dd>
+     * <dt>acceptEncoding</dt> <dd>An accept-encoding header</dd>
+     * <dt>ifMatch</dt> <dd>An if-match header</dd>
+     * <dt>ifNoneMatch</dt> <dd>An if-none-match header</dd>
+     * <dt>mimetypes</dt> <dd>A map of file/URI extenstions to mimetypes, these
+     * will be added to the default map of mimetypes</dd>
+     * <dt>baseUri</dt> <dd>The base relative URI to use when dispatcher isn't
+     * at the root of the domain. Do not put a trailing slash</dd>
+     * <dt>mounts</dt> <dd>an array of namespace to baseUri prefix mappings</dd>
+     * <dt>HTTPMethods</dt> <dd>an array of HTTP methods to support</dd>
+     * </dl>
+     *
+     * @param mixed[] config Configuration options
+     */
+    function __construct($config = array()) {
+        
+        // set defaults
+        $config['uri'] = parse_url($this->getConfig($config, 'uri', 'REQUEST_URI'), PHP_URL_PATH);
+        $config['baseUri'] = $this->getConfig($config, 'baseUri', '');
+        $config['accept'] = $this->getConfig($config, 'accept', 'HTTP_ACCEPT');
+        $config['acceptLang'] = $this->getConfig($config, 'acceptLang', 'HTTP_ACCEPT_LANGUAGE');
+        $config['acceptEncoding'] = $this->getConfig($config, 'acceptEncoding', 'HTTP_ACCEPT_ENCODING');
+        $config['ifMatch'] = $this->getConfig($config, 'ifMatch', 'HTTP_IF_MATCH');
+        $config['ifNoneMatch'] = $this->getConfig($config, 'ifNoneMatch', 'HTTP_IF_NONE_MATCH');
+        
+        if (isset($config['mimetypes']) && is_array($config['mimetypes'])) {
+            foreach ($config['mimetypes'] as $ext => $mimetype) {
+                $this->mimetypes[$ext] = $mimetype;
+            }
+        }
+        
+        // set baseUri
+        $this->baseUri = $config['baseUri'];
+        
+        // get request URI
+        $parts = explode('/', $config['uri']);
+        $lastPart = array_pop($parts);
+        $this->uri = join('/', $parts);
+        
+        $parts = explode('.', $lastPart);
+        $this->uri .= '/'.$parts[0];
+        
+        if ($this->uri != '/' && substr($this->uri, -1, 1) == '/') { // remove trailing slash problem
+            $this->uri = substr($this->uri, 0, -1);
+        }
+        
+        array_shift($parts);
+        foreach ($parts as $part) {
+            $this->accept[10][] = $part;
+            $this->acceptLang[10][] = $part;
+        }
+        
+        // sort accept headers
+        $accept = explode(',', strtolower($config['accept']));
+        foreach ($accept as $mimetype) {
+            $parts = explode(';q=', $mimetype);
+            if (isset($parts) && isset($parts[1]) && $parts[1]) {
+                $num = $parts[1] * 10;
+            } else {
+                $num = 10;
+            }
+            $key = array_search($parts[0], $this->mimetypes);
+            if ($key) {
+                $this->accept[$num][] = $key;
+            }
+        }
+        krsort($this->accept);
+        
+        // sort lang accept headers
+        $accept = explode(',', strtolower($config['acceptLang']));
+        foreach ($accept as $mimetype) {
+            $parts = explode(';q=', $mimetype);
+            if (isset($parts) && isset($parts[1]) && $parts[1]) {
+                $num = $parts[1] * 10;
+            } else {
+                $num = 10;
+            }
+            $this->acceptLang[$num][] = $parts[0];
+        }
+        krsort($this->acceptLang);
+        
+        // get encoding accept headers
+        if ($config['acceptEncoding']) {
+            foreach (explode(',', $config['acceptEncoding']) as $key => $accept) {
+                $this->acceptEncoding[$key] = trim($accept);
+            }
+        }
+        
+        // create negotiated URI lists from accept headers and request URI
+        foreach ($this->accept as $typeOrder) {
+            foreach ($typeOrder as $type) {
+                if ($type) {
+                    foreach ($this->acceptLang as $langOrder) {
+                        foreach ($langOrder as $lang) {
+                            if ($lang && $lang != $type) {
+                                $this->negotiatedUris[] = $this->uri.'.'.$type.'.'.$lang;
+                            }
+                        }
+                    }
+                    $this->negotiatedUris[] = $this->uri.'.'.$type;
+                    $this->formatNegotiatedUris[] = $this->uri.'.'.$type;
+                }
+            }
+        }
+        foreach ($this->acceptLang as $langOrder) {
+            foreach ($langOrder as $lang) {
+                if ($lang) {
+                    $this->negotiatedUris[] = $this->uri.'.'.$lang;
+                    $this->languageNegotiatedUris[] = $this->uri.'.'.$lang;
+                }
+            }
+        }
+        $this->negotiatedUris[] = $this->uri;
+        $this->formatNegotiatedUris[] = $this->uri;
+        $this->languageNegotiatedUris[] = $this->uri;
+        
+        $this->negotiatedUris = array_values(array_unique($this->negotiatedUris));
+        $this->formatNegotiatedUris = array_values(array_unique($this->formatNegotiatedUris));
+        $this->languageNegotiatedUris = array_values(array_unique($this->languageNegotiatedUris));
+        
+        $this->HTTPMethods = $this->getConfig($config, 'HTTPMethods', NULL, $this->HTTPMethods);
+        
+        // get HTTP method
+        $this->method = strtoupper($this->getConfig($config, 'method', 'REQUEST_METHOD', $this->method));
+        
+        // get HTTP request data
+        $this->data = $this->getConfig($config, 'data', NULL, file_get_contents("php://input"));
+        
+        // conditional requests
+        if ($config['ifMatch']) {
+            $ifMatch = explode(',', $config['ifMatch']);
+            foreach ($ifMatch as $etag) {
+                $this->ifMatch[] = trim($etag, '" ');
+            }
+        }
+        if ($config['ifNoneMatch']) {
+            $ifNoneMatch = explode(',', $config['ifNoneMatch']);
+            foreach ($ifNoneMatch as $etag) {
+                $this->ifNoneMatch[] = trim($etag, '" ');
+            }
+        }
+        
+        // mounts
+        if (isset($config['mount']) && is_array($config['mount'])) {
+            $this->mounts = $config['mount'];
+        }
+        
+        // prime named resources for autoloading
+        if (isset($config['autoload']) && is_array($config['autoload'])) {
+            foreach ($config['autoload'] as $uri => $className) {
+                $this->resources[$uri] = array(
+                    'class' => $className,
+                    'loaded' => FALSE
+                );
+            }
+        }
+        
+        // load definitions of already loaded resource classes
+        $resourceClassName = class_exists('Tonic\\Resource') ? 'Tonic\\Resource' : 'Resource';
+        foreach (get_declared_classes() as $className) {
+            if (is_subclass_of($className, $resourceClassName)) {
+                
+                $resourceDetails = $this->getResourceClassDetails($className);
+                
+                preg_match_all('/@uri\s+([^\s]+)(?:\s([0-9]+))?/', $resourceDetails['comment'], $annotations);
+                if (isset($annotations[1]) && $annotations[1]) {
+                    $uris = $annotations[1];
+                } else {
+                    $uris = array();
+                }
+                
+                foreach ($uris as $index => $uri) {
+                    if ($uri != '/' && substr($uri, -1, 1) == '/') { // remove trailing slash problem
+                        $uri = substr($uri, 0, -1);
+                    }
+                    if (isset($annotations[2][$index]) && is_numeric($annotations[2][$index])) {
+                        $priority = intval($annotations[2][$index]);
+                    } else {
+                        $priority = 0;
+                    }
+                    if (
+                        !isset($this->resources[$resourceDetails['mountPoint'].$uri]) ||
+                        $this->resources[$resourceDetails['mountPoint'].$uri]['priority'] < $priority
+                    ) {
+                        $this->resources[$resourceDetails['mountPoint'].$uri] = array(
+                            'namespace' => $resourceDetails['namespaceName'],
+                            'class' => $resourceDetails['className'],
+                            'filename' => $resourceDetails['filename'],
+                            'line' => $resourceDetails['line'],
+                            'priority' => $priority,
+                            'loaded' => TRUE
+                        );
+                    }
+                }
+            }
+        }
+        
+    }
+    
+    /**
+     * Get the details of a Resource class by reflection
+     * @param str className
+     * @return str[]
+     */
+    protected function getResourceClassDetails($className) {
+        
+        $resourceReflector = new ReflectionClass($className);
+        $comment = $resourceReflector->getDocComment();
+        
+        $className = $resourceReflector->getName();
+        if (method_exists($resourceReflector, 'getNamespaceName')) {
+            $namespaceName = $resourceReflector->getNamespaceName();
+        } else {
+            // @codeCoverageIgnoreStart
+            $namespaceName = FALSE;
+            // @codeCoverageIgnoreEnd
+        }
+        
+        if (!$namespaceName) {
+            preg_match('/@(?:package|namespace)\s+([^\s]+)/', $comment, $package);
+            if (isset($package[1])) {
+                $namespaceName = $package[1];
+            }
+        }
+        
+        // adjust URI for mountpoint
+        if (isset($this->mounts[$namespaceName])) {
+            $mountPoint = $this->mounts[$namespaceName];
+        } else {
+            $mountPoint = '';
+        }
+        
+        return array(
+            'comment' => $comment,
+            'className' => $className,
+            'namespaceName' => $namespaceName,
+            'filename' => $resourceReflector->getFileName(),
+            'line' => $resourceReflector->getStartLine(),
+            'mountPoint' => $mountPoint
+        );
+    
+    }
+    
+    /**
+     * Convert the object into a string suitable for printing
+     * @return str
+     * @codeCoverageIgnore
+     */
+    function __toString() {
+        $str = 'URI: '.$this->uri."\n";
+        $str .= 'Method: '.$this->method."\n";
+        if ($this->data) {
+            $str .= 'Data: '.$this->data."\n";
+        }
+        $str .= 'Acceptable Formats:';
+        foreach ($this->accept as $accept) {
+            foreach ($accept as $a) {
+                $str .= ' .'.$a;
+                if (isset($this->mimetypes[$a])) $str .= ' ('.$this->mimetypes[$a].')';
+            }
+        }
+        $str .= "\n";
+        $str .= 'Acceptable Languages:';
+        foreach ($this->acceptLang as $accept) {
+            foreach ($accept as $a) {
+                $str .= ' '.$a;
+            }
+        }
+        $str .= "\n";
+        $str .= 'Negotated URIs:'."\n";
+        foreach ($this->negotiatedUris as $uri) {
+            $str .= "\t".$uri."\n";
+        }
+        $str .= 'Format Negotated URIs:'."\n";
+        foreach ($this->formatNegotiatedUris as $uri) {
+            $str .= "\t".$uri."\n";
+        }
+        $str .= 'Language Negotated URIs:'."\n";
+        foreach ($this->languageNegotiatedUris as $uri) {
+            $str .= "\t".$uri."\n";
+        }
+        if ($this->ifMatch) {
+            $str .= 'If Match:';
+            foreach ($this->ifMatch as $etag) {
+                $str .= ' '.$etag;
+            }
+            $str .= "\n";
+        }
+        if ($this->ifNoneMatch) {
+            $str .= 'If None Match:';
+            foreach ($this->ifNoneMatch as $etag) {
+                $str .= ' '.$etag;
+            }
+            $str .= "\n";
+        }
+        $str .= 'Loaded Resources:'."\n";
+        foreach ($this->resources as $uri => $resource) {
+            $str .= "\t".$uri."\n";
+            if (isset($resource['namespace']) && $resource['namespace']) $str .= "\t\tNamespace: ".$resource['namespace']."\n";
+            $str .= "\t\tClass: ".$resource['class']."\n";
+            $str .= "\t\tFile: ".$resource['filename'];
+            if (isset($resource['line']) && $resource['line']) $str .= '#'.$resource['line'];
+            $str .= "\n";
+        }
+        return $str;
+    }
+    
+    /**
+     * Instantiate the resource class that matches the request URI the best
+     * @return Resource
+     * @throws ResponseException If the resource does not exist, a 404 exception is thrown
+     */
+    function loadResource() {
+        
+        $uriMatches = array();
+        foreach ($this->resources as $uri => $resource) {
+            
+            preg_match_all('#((?<!\?):[^/]+|{[^0-9][^}]*}|\(.+?\))#', $uri, $params, PREG_PATTERN_ORDER);
+            
+            $uri = $this->baseUri.$uri;
+            if ($uri != '/' && substr($uri, -1, 1) == '/') { // remove trailing slash problem
+                $uri = substr($uri, 0, -1);
+            }
+            $uriRegex = preg_replace('#((?<!\?):[^(/]+|{[^0-9][^}]*})#', '(.+)', $uri);
+            
+            if (preg_match('#^'.$uriRegex.'$#', $this->uri, $matches)) {
+                array_shift($matches);
+                
+                if (isset($params[1])) {
+                    foreach ($params[1] as $index => $param) {
+                        if (isset($matches[$index])) {
+                            if (substr($param, 0, 1) == ':') {
+                                $matches[substr($param, 1)] = $matches[$index];
+                                unset($matches[$index]);
+                            } elseif (substr($param, 0, 1) == '{' && substr($param, -1, 1) == '}') {
+                                $matches[substr($param, 1, -1)] = $matches[$index];
+                                unset($matches[$index]);
+                            }
+                        }
+                    }
+                }
+                
+                $uriMatches[isset($resource['priority']) ? $resource['priority'] : 0] = array(
+                    $uri,
+                    $resource,
+                    $matches
+                );
+                
+            }
+        }
+        krsort($uriMatches);
+        
+        if ($uriMatches) {
+            list($uri, $resource, $parameters) = array_shift($uriMatches);
+            if (!$resource['loaded']) { // autoload
+                //echo $resource['class'];
+                if (!class_exists($resource['class'])) {
+                    throw new Exception('Unable to load resource');
+                }
+                $resourceDetails = $this->getResourceClassDetails($resource['class']);
+                $resource = $this->resources[$uri] = array(
+                    'namespace' => $resourceDetails['namespaceName'],
+                    'class' => $resourceDetails['className'],
+                    'filename' => $resourceDetails['filename'],
+                    'line' => $resourceDetails['line'],
+                    'priority' => 0,
+                    'loaded' => TRUE
+                );
+            }
+            
+            $this->allowedMethods = array_intersect(array_map('strtoupper', get_class_methods($resource['class'])), $this->HTTPMethods);
+            
+            return new $resource['class']($parameters);
+        }
+        
+        // no resource found, throw response exception
+        throw new ResponseException('A resource matching URI "'.$this->uri.'" was not found', Response::NOTFOUND);
+        
+    }
+    
+    /**
+     * Check if an etag matches the requests if-match header
+     * @param str etag Etag to match
+     * @return bool
+     */
+    function ifMatch($etag) {
+        if (isset($this->ifMatch[0]) && $this->ifMatch[0] == '*') {
+            return TRUE;
+        }
+        return in_array($etag, $this->ifMatch);
+    }
+    
+    /**
+     * Check if an etag matches the requests if-none-match header
+     * @param str etag Etag to match
+     * @return bool
+     */
+    function ifNoneMatch($etag) {
+        if (isset($this->ifMatch[0]) && $this->ifMatch[0] == '*') {
+            return FALSE;
+        }
+        return in_array($etag, $this->ifNoneMatch);
+    }
+    
+    /**
+     * Return the most acceptable of the given formats based on the accept array
+     * @param str[] formats
+     * @param str default The default format if the requested format does not match $formats
+     * @return str
+     */
+    function mostAcceptable($formats, $default = NULL) {
+        foreach (call_user_func_array('array_merge', $this->accept) as $format) {
+            if (in_array($format, $formats)) {
+                return $format;
+            }
+        }
+        return $default;
+    }
+    
+}
+
+/**
+ * Base resource class
+ * @namespace Tonic\Lib
+ */
+class Resource {
+    
+    private $parameters;
+    
+    /**
+     * Resource constructor
+     * @param str[] parameters Parameters passed in from the URL as matched from the URI regex
+     */
+    function  __construct($parameters) {
+        $this->parameters = $parameters;
+    }
+    
+    /**
+     * Convert the object into a string suitable for printing
+     * @return str
+     * @codeCoverageIgnore
+     */
+    function __toString() {
+        $str = get_class($this);
+        foreach ($this->parameters as $name => $value) {
+            $str .= "\n".$name.': '.$value;
+        }
+        return $str;
+    }
+    
+    /**
+     * Execute a request on this resource.
+     * @param Request request The request to execute the resource in the context of
+     * @return Response
+     * @throws ResponseException If the HTTP method is not allowed on the resource, a 405 exception is thrown
+     */
+    function exec($request) {
+        
+        if (
+            in_array(strtoupper($request->method), $request->HTTPMethods) &&
+            method_exists($this, $request->method)
+        ) {
+            
+            $method = new ReflectionMethod($this, $request->method);
+            $parameters = array();
+            foreach ($method->getParameters() as $param) {
+                if ($param->name == 'request') {
+                    $parameters[] = $request;
+                } elseif (isset($this->parameters[$param->name])) {
+                    $parameters[] = $this->parameters[$param->name];
+                    unset($this->parameters[$param->name]);
+                } else {
+                    $parameters[] = reset($this->parameters);
+                    array_shift($this->parameters);
+                }
+            }
+            
+            $response = call_user_func_array(
+                array($this, $request->method),
+                $parameters
+            );
+            
+            $responseClassName = class_exists('Tonic\\Response') ? 'Tonic\\Response' : 'Response';
+            if (!$response || !($response instanceof $responseClassName)) {
+                throw new Exception('Method '.$request->method.' of '.get_class($this).' did not return a Response object');
+            }
+            
+        } else {
+            
+            // send 405 method not allowed
+            throw new ResponseException(
+                'The HTTP method "'.$request->method.'" is not allowed for the resource "'.$request->uri.'".',
+                Response::METHODNOTALLOWED
+            );
+            
+        }
+        
+        # good for debugging, remove this at some point
+        $response->addHeader('X-Resource', get_class($this));
+        
+        return $response;
+        
+    }
+    
+}
+
+/**
+ * Model the data of the outgoing HTTP response
+ * @namespace Tonic\Lib
+ */
+class Response {
+    
+    /**
+     * HTTP response code constant
+     */
+    const OK = 200,
+          CREATED = 201,
+          NOCONTENT = 204,
+          MOVEDPERMANENTLY = 301,
+          FOUND = 302,
+          SEEOTHER = 303,
+          NOTMODIFIED = 304,
+          TEMPORARYREDIRECT = 307,
+          BADREQUEST = 400,
+          UNAUTHORIZED = 401,
+          FORBIDDEN = 403,
+          NOTFOUND = 404,
+          METHODNOTALLOWED = 405,
+          NOTACCEPTABLE = 406,
+          GONE = 410,
+          LENGTHREQUIRED = 411,
+          PRECONDITIONFAILED = 412,
+          UNSUPPORTEDMEDIATYPE = 415,
+          INTERNALSERVERERROR = 500;
+    
+    /**
+     * The request object generating this response
+     * @var Request
+     */
+    public $request;
+    
+    /**
+     * The HTTP response code to send
+     * @var int
+     */
+    public $code = Response::OK;
+    
+    /**
+     * The HTTP headers to send
+     * @var str[]
+     */
+    public $headers = array();
+    
+    /**
+     * The HTTP response body to send
+     * @var str
+     */
+    public $body;
+    
+    /**
+     * Create a response object.
+     * @param Request request The request object generating this response
+     * @param str uri The URL of the actual resource being used to build the response
+     */
+    function __construct($request, $uri = NULL) {
+        
+        $this->request = $request;
+        
+        if ($uri && $uri != $request->uri) { // add content location header
+            $this->addHeader('Content-Location', $uri);
+            $this->addVary('Accept');
+            $this->addVary('Accept-Language');
+        }
+        $this->addHeader('Allow', implode(', ', $request->allowedMethods));
+        
+    }
+    
+    /**
+     * Convert the object into a string suitable for printing
+     * @return str
+     * @codeCoverageIgnore
+     */
+    function __toString() {
+        $str = 'HTTP/1.1 '.$this->code;
+        foreach ($this->headers as $name => $value) {
+            $str .= "\n".$name.': '.$value;
+        }
+        return $str;
+    }
+    
+    /**
+     * Add a header to the response
+     * @param str header
+     * @param str value
+     */
+    function addHeader($header, $value) {
+        $this->headers[$header] = $value;
+    }
+    
+    /**
+     * Send a cache control header with the response
+     * @param int time Cache length in seconds
+     */
+    function addCacheHeader($time = 86400) {
+        if ($time) {
+            $this->addHeader('Cache-Control', 'max-age='.$time.', must-revalidate');
+        } else {
+            $this->addHeader('Cache-Control', 'no-cache');
+        }
+    }
+    
+    /**
+     * Send an etag with the response
+     * @param str etag Etag value
+     */
+    function addEtag($etag) {
+        $this->addHeader('Etag', '"'.$etag.'"');
+    }
+    
+    function addVary($header) {
+        if (isset($this->headers['Vary'])) {
+            $this->headers['Vary'] .= ', '.$header;
+        } else {
+            $this->addHeader('Vary', $header);
+        }
+    }
+    
+    /**
+     * Output the response
+     * @codeCoverageIgnore
+     */
+    function output() {
+        
+        if (php_sapi_name() != 'cli' && !headers_sent()) {
+            
+            header('HTTP/1.1 '.$this->code);
+            foreach ($this->headers as $header => $value) {
+                header($header.': '.$value);
+            }
+        }
+        
+        if (strtoupper($this->request->method) !== 'HEAD') {
+            echo $this->body;
+        }
+        
+    }
+    
+}
+
+/**
+ * Exception class for HTTP response errors
+ * @namespace Tonic\Lib
+ */
+class ResponseException extends Exception {
+    
+    /**
+     * Generate a default response for this exception
+     * @param Request request
+     * @return Response
+     */
+    function response($request) {
+        $response = new Response($request);
+        $response->code = $this->code;
+        $response->body = $this->message;
+        return $response;
+    }
+    
+}
+
Index: /sandbox/webservice/api/library/tonic/docroot/dispatch.php
===================================================================
--- /sandbox/webservice/api/library/tonic/docroot/dispatch.php	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/docroot/dispatch.php	(revision 6019)
@@ -0,0 +1,26 @@
+<?php
+
+// load Tonic library
+require_once '../lib/tonic.php';
+
+// load examples
+require_once '../examples/examples.php';
+
+// handle request
+$request = new Request();
+try {
+    $resource = $request->loadResource();
+    $response = $resource->exec($request);
+
+} catch (ResponseException $e) {
+    switch ($e->getCode()) {
+    case Response::UNAUTHORIZED:
+        $response = $e->response($request);
+        $response->addHeader('WWW-Authenticate', 'Basic realm="Tonic"');
+        break;
+    default:
+        $response = $e->response($request);
+    }
+}
+$response->output();
+
Index: /sandbox/webservice/api/library/tonic/docroot/.htaccess
===================================================================
--- /sandbox/webservice/api/library/tonic/docroot/.htaccess	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/docroot/.htaccess	(revision 6019)
@@ -0,0 +1,6 @@
+<IfModule mod_rewrite.c>
+    RewriteEngine On
+    RewriteCond %{REQUEST_URI} !dispatch\.php$
+    RewriteCond %{REQUEST_FILENAME} !-f
+    RewriteRule .* dispatch.php [L,QSA]
+</IfModule>
Index: /sandbox/webservice/api/library/tonic/README.markdown
===================================================================
--- /sandbox/webservice/api/library/tonic/README.markdown	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/README.markdown	(revision 6019)
@@ -0,0 +1,233 @@
+PHP library/framework for building Web apps while respecting the 5 principles
+of RESTful design.
+
+ * Give every "thing" an ID (aka URIs)
+ * Link things together (HATEOAS)
+ * Use standard methods (aka the standard interface)
+ * Resources with multiple representations (aka standard document formats)
+ * Communicate statelessly
+
+[See the Tonic site for more info](http://peej.github.com/tonic/).
+
+
+How it works
+============
+
+Everything is a resource, and a resource is defined as a PHP class. An annotation
+wires a URI (or a collection of URIs) to the resource, and methods that match
+the HTTP methods by name allow interaction with it.
+
+    /**
+     * This class defines an example resource that is wired into the URI /example
+     * @uri /example
+     */
+    class ExampleResource extends Resource { }
+
+The incoming HTTP request is turned into a list of negotiated URIs based on the
+accept request headers which can then be used to pick the best representation
+for the response.
+
+    /**
+     * This class defines an example resource that is wired into the URI /example
+     * @uri /example
+     */
+    class ExampleResource extends Resource {
+        
+        function get($request) {
+            
+            $response = new Response($request);
+            
+            $response->code = Response::OK;
+            $response->body = 'Example response';
+            
+            return $response;
+            
+        }
+      
+    }
+
+
+How to get started
+==================
+
+The best place to get started is to get the hello world example running on your
+system, to do this you will need a web server running PHP5.1+.
+
+Place all of the Tonic files into your PHP include path so that other scripts can
+find it. By default on Windows this will probably be in "c:\php\includes\tonic" or
+on Linux/Unix it will be "/usr/share/php/tonic"
+
+Copy "docroot/dispatch.php" into your servers document root and edit it so that the
+require_once statement paths point to the Tonic library and the examples.
+
+Finally you need to route all incoming requests to dispatch.php. How you do this
+depends on your web server. If you are using Apache, the simplest way is to copy
+the .htaccess file from "docroot/.htaccess" into your Apache document root.
+
+
+Features
+========
+
+
+Request URI
+-----------
+
+The URI that is processed for the request when you create the Tonic Request object
+is gather by default from the REQUEST_URI Apache variable. If you need to gather
+the URI from another $_SERVER variable or somewhere else then you can pass it into
+the Request objects constructor as a configuration option:
+
+    $request = new Request(array(
+        'uri' => $_SERVER['PATH_INFO']
+    ));
+
+
+Base URI
+--------
+
+If you want to put your Tonic dispatcher at a URL that isn't the root of a domain
+then you'll need to let the Request object know so that the @uri annotations ignore
+it:
+
+    $request = new Request(array(
+        'baseUri' => '/some/base/uri'
+    ));
+
+Don't put a trailing slash on the end.
+
+
+URI annotations
+---------------
+
+Resources are attached to their URL by their @uri annotation:
+
+    /**
+     * @uri /example
+     */
+    class ExampleResource extends Resource { }
+
+As well as a straight forward URI string, you can also use a regular expression
+so that a resource is tied to a range of URIs:
+
+    /**
+     * @uri /example/([a-z]+)
+     */
+    class ExampleResource extends Resource {
+        function get($request, $parameter) {
+            ...
+        }
+    }
+
+URL template and Rails route style @uri annotations are also supported:
+
+    /**
+     * @uri /users/{username}
+     */
+    class ExampleResource extends Resource {
+        function get($request, $username) {
+            ...
+        }
+    }
+    
+    /**
+     * @uri /users/:username
+     */
+    class ExampleResource extends Resource {
+        function get($request, $username) {
+            ...
+        }
+    }
+
+It is also possible for multiple resource to match the same URI, so you can
+prioritise which resource should be used by specifying a priority level as part
+of the annotation:
+
+    /**
+     * @uri /example/([a-z]+)
+     */
+    class ExampleResource extends Resource { }
+
+    /**
+     * @uri /example/apple 2
+     */
+    class ExampleResource extends Resource { }
+
+By postfixing the @uri annotation with a number, of all the matching resources,
+the one with the highest postfixed number will be used.
+
+
+Mimetypes
+---------
+
+To handle content negotiation via filename style extensions to URLs as well the
+HTTP Accept header, a mapping between extensions and mimetypes can be provided.
+By default this list contains a number of common mappings, if you need to add one
+or more of your own, pass them into the constructor as an array:
+
+    $request = new Request(array(
+        'mimetypes' => array(
+            'ogv' => 'video/ogg'
+        )
+    ));
+
+
+Mount points
+------------
+
+To make resources more portable, it is possible to "mount" them into your URL-space
+by providing a namespace name to URL-space mapping. Every resource within that
+namespace will in effect have the URL-space prefixed to their @uri annotation.
+
+    $request = new Request(array(
+        'mount' => array(
+            'namespaceName' => '/some/mounted/uri'
+        )
+    ));
+
+Again, don't put a trailing slash on the end, and if you aren't using PHP5.3 and
+namespaces, you can use the @namespace annotation.
+
+
+Response exceptions
+-------------------
+
+The Request object and Resource objects can throw ResponseExceptions when a problem
+occurs that the object does not want to handle and so relinquishes control back
+to the dispatcher.
+
+The ResponseException has its code value set to the HTTP response code of the problem
+and its message set to a human readable reason for throwing the exception. The
+ResponseException::response() method can be used to produce a default Response object
+expressing the exception if required.
+
+The Request object throws a 404 ResponseException when the resource to be loaded
+does not exist.
+
+The Resource object throws a 405 ResponseException when the HTTP method from the
+request is not able to be handled by the resource.
+
+If you don't want to handle a problem within your Resource class, you can throw your
+own ResponseException and handle it in the dispatcher. Look at the auth example for
+an example of how.
+
+
+Autoloading classes
+-------------------
+
+If you've got lots of resource classes and don't fancy including them all in your
+dispatcher, you can use the autoload function to load a resource class for a given
+URL-space.
+
+    $request = new Request(array(
+        'autoload' => array(
+            '/example/[a-z]+' => 'ClassName'
+        )
+    ));
+
+In this example, the class ClassName will be autoloaded via [the standard PHP
+__autoload method](http://php.net/manual/en/language.oop5.autoload.php).
+
+
+
+For more information, read the code. Start with the dispatcher "docroot/dispatch.php"
+and then the examples in the "examples" directory.
Index: /sandbox/webservice/api/library/tonic/examples/smarty/representations/default.html
===================================================================
--- /sandbox/webservice/api/library/tonic/examples/smarty/representations/default.html	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/examples/smarty/representations/default.html	(revision 6019)
@@ -0,0 +1,2 @@
+<p>This is the default content for the Smarty resource.</p>
+<p>Today is {""|@time|date_format}</p>
Index: /sandbox/webservice/api/library/tonic/examples/smarty/representations/shell.html
===================================================================
--- /sandbox/webservice/api/library/tonic/examples/smarty/representations/shell.html	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/examples/smarty/representations/shell.html	(revision 6019)
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>{$title|escape}</title>
+    </head>
+    <body>
+        <h1>{$title|escape}</h1>
+        {$body}
+    </body>
+</html>
Index: /sandbox/webservice/api/library/tonic/examples/smarty/smarty.php
===================================================================
--- /sandbox/webservice/api/library/tonic/examples/smarty/smarty.php	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/examples/smarty/smarty.php	(revision 6019)
@@ -0,0 +1,60 @@
+<?php
+
+@include_once 'smarty/Smarty.class.php';
+
+/**
+ * Smarty template example
+ *
+ * Using Smarty for representation generation
+ *
+ * @namespace Tonic\Examples\Filesystem
+ * @uri /smarty
+ */
+class SmartyResource extends Resource {
+    
+    protected $smarty;
+    
+    function __construct() {
+        
+        $this->smarty = new Smarty();
+        $this->smarty->template_dir = '../examples/smarty/representations';
+        $this->smarty->compile_dir = sys_get_temp_dir();
+        
+    }
+    
+    function get($request) {
+        
+        $response = new Response($request);
+        
+        $this->smarty->assign('title', 'Smarty template');
+        $body = $this->render('default');
+        
+        $etag = md5($body);
+        if ($request->ifNoneMatch($etag)) {
+            
+            $response->code = Response::NOTMODIFIED;
+            
+        } else {
+        
+            $response->code = Response::OK;
+            $response->addHeader('Content-type', 'text/html');
+            $response->body = $body;
+            
+        }
+        
+        return $response;
+        
+    }
+    
+    protected function render($view, $format = 'html', $useShell = TRUE) {
+        
+        if ($format == 'html' && $useShell) {
+            $this->smarty->assign('body', $this->smarty->fetch($view.'.'.$format));
+            return $this->smarty->fetch('shell.'.$format);
+        } else {
+            return $this->smarty->fetch($view.'.'.$format);
+        }
+        
+    }
+    
+}
Index: /sandbox/webservice/api/library/tonic/examples/auth/auth.php
===================================================================
--- /sandbox/webservice/api/library/tonic/examples/auth/auth.php	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/examples/auth/auth.php	(revision 6019)
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * Authentication example
+ *
+ * An example authentication resource, the isSecured() method can be used to ensure
+ * that only authorised users can access the resource.
+ *
+ * username: user
+ * password: pass
+ *
+ * @namespace Tonic\Examples\Auth
+ * @uri /auth
+ */
+class AuthResource extends Resource {
+    
+    const USERNAME = 'user';
+    const PASSWORD = 'pass';
+    
+    function isSecured() {
+        
+        if (
+            isset($_SERVER['PHP_AUTH_USER']) && $_SERVER['PHP_AUTH_USER'] == AuthResource::USERNAME &&
+            isset($_SERVER['PHP_AUTH_PW']) && $_SERVER['PHP_AUTH_PW'] == AuthResource::PASSWORD
+        ) {
+            return;
+        }
+        
+        throw new ResponseException('Incorrect username and password', Response::UNAUTHORIZED);
+        
+    }
+    
+    /**
+     * Handle a GET request for this resource
+     * @param Request request
+     * @return Response
+     */
+    function get($request) {
+        
+        $this->isSecured();
+        
+        $response = new Response($request);
+        
+        $response->body = 'You have access to the secret';
+        
+        return $response;
+        
+    }
+    
+}
+
Index: /sandbox/webservice/api/library/tonic/examples/multirep/multirep.php
===================================================================
--- /sandbox/webservice/api/library/tonic/examples/multirep/multirep.php	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/examples/multirep/multirep.php	(revision 6019)
@@ -0,0 +1,99 @@
+<?php
+
+/**
+ * Multiple representation example
+ *
+ * A representation is an output format for a resource. A single resource can have
+ * many output representations that can differ depending on the request data.
+ *
+ * Representations are built up as strings within the Response object and it is
+ * down to the programmer to implement the desired logic for choosing and building
+ * a required representation.
+ *
+ * This example produces three different representations of the resource data
+ * depending on the request accept header information.
+ *
+ * @namespace Tonic\Examples\Multirep
+ * @uri /multirep
+ */
+class MultiRepresentationResource extends Resource {
+    
+    var $data = array(
+        'secret' => 'monkey',
+        'hidden' => 'squirrel'
+    );
+    
+    /**
+     * Handle a GET request for this resource
+     * @param Request request
+     * @return Response
+     */
+    function get($request) {
+        
+        $response = new Response($request);
+        $response->code = Response::OK;
+        
+        // select the most acceptable format of the given types from the request
+        $format = $request->mostAcceptable(array(
+            'json', 'html', 'txt'
+        ));
+        
+        switch ($format) {
+        
+        // obviously it might make more sense to put the HTML in another file
+        // and use some kind of templating system
+        case 'html':
+            $response->addHeader('Content-type', 'text/html');
+            $response->body = '<!DOCTYPE html>';
+            $response->body .= '<html>';
+            $response->body .= '<head>';
+            $response->body .= '<title>HTML Representation</title>';
+            $response->body .= '</head>';
+            $response->body .= '<body>';
+            $response->body .= '<h1>HTML Representation</h1>';
+            $response->body .= '<table border="1">';
+            foreach ($this->data as $field => $value) {
+                $response->body .= '<tr>';
+                $response->body .= '<th>'.htmlspecialchars($field).'</th>';
+                $response->body .= '<td>'.htmlspecialchars($value).'</td>';
+                $response->body .= '</tr>';
+            }
+            $response->body .= '</table>';
+            $response->body .= '<h2>Explicit links to available formats</h2>';
+            $response->body .= '<a href="/multirep.txt">Plain text</a> ';
+            $response->body .= '<a href="/multirep.json">JSON</a> ';
+            $response->body .= '<a href="/multirep.html">HTML</a>';
+            $response->body .= '</body>';
+            $response->body .= '</html>';
+            break;
+        
+        case 'json':
+            $response->addHeader('Content-type', 'application/json');
+            $response->body = json_encode($this->data);
+            break;
+        
+        case 'txt':
+            $response->addHeader('Content-type', 'text/plain');
+            $response->body = "Plain Text Representation\n";
+            $response->body .= "\n";
+            ob_start();
+            var_dump($this->data);
+            $response->body .= ob_get_contents();
+            ob_end_clean();
+            break;
+            
+        // we don't have a suitable format, so do a 406 response instead
+        default:
+            $response->code = Response::NOTACCEPTABLE;
+            $response->addHeader('Content-type', 'text/plain');
+            $response->body = "406 Not Acceptable\n";
+            break;
+            
+        }
+        
+        return $response;
+        
+    }
+    
+}
+
Index: /sandbox/webservice/api/library/tonic/examples/examples.php
===================================================================
--- /sandbox/webservice/api/library/tonic/examples/examples.php	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/examples/examples.php	(revision 6019)
@@ -0,0 +1,60 @@
+<?php
+
+require_once 'helloworld/helloworld.php';
+require_once 'filesystem/filesystem.php';
+require_once 'filesystem/filesystemcollection.php';
+require_once 'smarty/smarty.php';
+require_once 'htmlform/htmlform.php';
+require_once 'auth/auth.php';
+require_once 'multirep/multirep.php';
+
+/**
+ * Examples listing
+ * @namespace Tonic\Examples
+ * @uri /
+ */
+class ExamplesListResource extends Resource {
+    
+    function get($request) {
+        
+        $response = new Response($request);
+        
+        $examples = '';
+        $dirs = glob(dirname(__FILE__).DIRECTORY_SEPARATOR.'*', GLOB_ONLYDIR);
+        if ($dirs) {
+            foreach ($dirs as $path) {
+                $location = basename($path);
+                $readme = $path.DIRECTORY_SEPARATOR.$location.'.php';
+                if (file_exists($readme)) {
+                    preg_match('|/\*\*\s*\*\s*(.+?)\*/|s', file_get_contents($readme), $match);
+                    $comment = preg_replace('|\s*\*\s*(@.+)?|', "\n", $match[1]);
+                    $parts = explode("\n\n", $comment);
+                    $name = array_shift($parts);
+                    $description = join(' ', $parts);
+                } else {
+                    $name = $location;
+                    $description = '';
+                }
+                $examples .= 
+                    '<li>'.
+                    '<h3><a href="'.$location.'">'.$name.'</a></h3>'.
+                    '<p>'.$description.'</p>'.
+                    '</li>';
+            }
+        } else {
+            $examples .= '<li>No examples</li>';
+        }
+        
+        $response->body = <<<END
+<h1>Welcome to Tonic</h1>
+<p>Below is a list of example uses of the Tonic library. View each example to see it in action.</p>
+<h2>Examples</h2>
+END;
+        $response->body .= '<ul>'.$examples.'</ul>';
+        
+        return $response;
+        
+    }
+    
+}
+
Index: /sandbox/webservice/api/library/tonic/examples/filesystem/representations/default.html
===================================================================
--- /sandbox/webservice/api/library/tonic/examples/filesystem/representations/default.html	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/examples/filesystem/representations/default.html	(revision 6019)
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>Default Document</title>
+    </head>
+    <body>
+        <p>Welcome to the filesystem example.</p>
+        <p>The resource "FilesystemResource" in filesystem.php is wired to any URI starting /filesystem and then maps the remaining part of the URI to the filenames on the filesystem within "examples/filesystem/representations".</p>
+        <p>This page is being read from "examples/filesystem/default.html" on the filesystem as it is the default document
+        for the current URL. <a href="filesystem/default.html">You'll also be able to see it at this URL</a>.</p>
+        <p>The FilesystemResource also allows updating of the files by sending PUT and DELETE requests to their mapped URI.</p>
+        <p>The resource "FilesystemCollection" in filesystemcollection.php 
+        <p><a href="filesystem/collection">View a collection resource for the directory "examples/filesystem/representations/collection"</a>.</p>
+        <p>The FilesystemCollection also allows adding of a file to the collection by sending a POST request to the collection URI.</p>
+    </body>
+</html>
Index: /sandbox/webservice/api/library/tonic/examples/filesystem/representations/collection/one.txt
===================================================================
--- /sandbox/webservice/api/library/tonic/examples/filesystem/representations/collection/one.txt	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/examples/filesystem/representations/collection/one.txt	(revision 6019)
@@ -0,0 +1,1 @@
+One
Index: /sandbox/webservice/api/library/tonic/examples/filesystem/representations/collection/one.html
===================================================================
--- /sandbox/webservice/api/library/tonic/examples/filesystem/representations/collection/one.html	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/examples/filesystem/representations/collection/one.html	(revision 6019)
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>One</title>
+    </head>
+    <body>
+        One
+    </body>
+</html>
Index: /sandbox/webservice/api/library/tonic/examples/filesystem/representations/collection/two.html
===================================================================
--- /sandbox/webservice/api/library/tonic/examples/filesystem/representations/collection/two.html	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/examples/filesystem/representations/collection/two.html	(revision 6019)
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>Two</title>
+    </head>
+    <body>
+        Two
+    </body>
+</html>
Index: /sandbox/webservice/api/library/tonic/examples/filesystem/filesystemcollection.php
===================================================================
--- /sandbox/webservice/api/library/tonic/examples/filesystem/filesystemcollection.php	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/examples/filesystem/filesystemcollection.php	(revision 6019)
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * Collection resource
+ * @namespace Tonic\Examples\Filesystem
+ * @uri /filesystem/collection
+ */
+class FilesystemCollection extends FilesystemResource {
+    
+    /**
+     * Path to the collections files
+     * @var str
+     */
+    var $collection;
+    
+    function __construct($parameters) {
+        parent::__construct($parameters);
+        $this->collection = dirname(__FILE__).'/representations/collection';
+    }
+    
+    /**
+     * Handle a GET request
+     * @param Request request
+     * @return Response
+     */
+    function get($request) {
+        
+        $response = new Response($request);
+        $collection = str_replace('/', DIRECTORY_SEPARATOR, $this->collection);
+        
+        $resourceUris = '';
+        $files = glob($collection.DIRECTORY_SEPARATOR.'*');
+        if ($files) {
+            foreach ($files as $filepath) {
+            	$filepath = str_replace(DIRECTORY_SEPARATOR, '/', $filepath);
+                $resourceUris .= '<li><a href="'.$this->turnFilePathIntoUri($filepath, $request).'">'.basename($filepath).'</a></li>';
+            }
+        } else {
+            $resourceUris .= '<li>Empty collection</li>';
+        }
+        
+        $response->body = '<ul>'.$resourceUris.'</ul>';
+        
+        return $response;
+        
+    }
+    
+    protected function getNextAvailableItemUri() {
+    	$collection = str_replace('/', DIRECTORY_SEPARATOR, $this->collection);
+        $filename = 1;
+        while (file_exists($collection.DIRECTORY_SEPARATOR.$filename)) {
+            $filename++;
+        }
+        return $this->uriStub.substr($this->collection, strlen($this->path)).'/'.$filename;
+    }
+    
+    function post($request) {
+        
+        $response = new Response($request);
+        
+        if ($request->data) {
+            $uri = $this->getNextAvailableItemUri();
+            $filePath = $this->turnUriIntoFilePath($uri, $request);
+            if (file_put_contents($filePath, $request->data)) {
+                $response->code = Response::CREATED;
+                $response->addHeader('Location', $uri);
+            } else {
+                $response->code = Response::INTERNALSERVERERROR;
+            }
+        } else {
+            $response->code = Response::LENGTHREQUIRED;
+        }
+        
+        return $response;
+        
+    }
+    
+}
+
Index: /sandbox/webservice/api/library/tonic/examples/filesystem/filesystem.php
===================================================================
--- /sandbox/webservice/api/library/tonic/examples/filesystem/filesystem.php	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/examples/filesystem/filesystem.php	(revision 6019)
@@ -0,0 +1,160 @@
+<?php
+
+/**
+ * Use the filesystem as resource representations
+ *
+ * This example loads files from the filesystem to be used as resource representations
+ * and provides a resource collection to contain them within. It also demonstrates
+ * the ability to add resources to the collection, and update and delete resources
+ * from within the collection.
+ *
+ * @namespace Tonic\Examples\Filesystem
+ * @uri /filesystem(/.*)?
+ */
+class FilesystemResource extends Resource {
+    
+    /**
+     * Path to the files to use
+     * @var str
+     */
+    var $path;
+    
+    /**
+     * URI stub
+     * @var str
+     */
+    var $uriStub = '/filesystem/';
+    
+    /**
+     * The default document to use if the request is for a URI that maps to a directory
+     * @var str
+     */
+    var $defaultDocument = 'default.html';
+    
+    function __construct($parameters) {
+        parent::__construct($parameters);
+        $this->path = dirname(__FILE__).'/representations/';
+    }
+    
+    protected function turnUriIntoFilePath($uri, $request) {
+        return $this->path.DIRECTORY_SEPARATOR.substr($uri, strlen($request->baseUri.$this->uriStub));
+    }
+    
+    protected function turnFilePathIntoUri($path, $request) {
+        return $request->baseUri.$this->uriStub.substr($path, strlen($this->path));
+    }
+    
+    /**
+     * Handle a GET request for this resource by returning the contents of a file matching the request URI
+     * @param Request request
+     * @return Response
+     */
+    function get($request) {
+        
+        // look at all candidate URIs in turn and stop when we find a file that matches one
+        foreach ($request->negotiatedUris as $uri) {
+            
+            // convert URI into filesystem path
+            $filePath = $this->turnUriIntoFilePath($uri, $request);
+            
+            if (substr($filePath, -1, 1) == DIRECTORY_SEPARATOR) { // add a default filename to the path
+                $filePath .= $this->defaultDocument;
+                $uri .= $this->defaultDocument;
+            }
+            
+            if (file_exists($filePath)) { // use this file
+                
+                $response = new Response($request, $uri);
+                
+                // generate etag for the resource based on the files modified date
+                $etag = md5(filemtime($filePath));
+                if ($request->ifNoneMatch($etag)) { // client has matching etag
+                    
+                    $response->code = Response::NOTMODIFIED;
+                    
+                } else {
+                    
+                    $explodedPath = explode('.', $filePath);
+                    $extension = array_pop($explodedPath);
+                    if (isset($request->mimetypes[$extension])) { // add content type header
+                        $response->addHeader('Content-Type', $request->mimetypes[$extension]);
+                    }
+                    
+                    $response->addEtag($etag); // add etag header
+                    
+                    $response->body = file_get_contents($filePath); // set contents
+                    
+                }
+                return $response;
+                
+            }
+            
+        }
+        
+        // nothing found, send 404 response
+        $response = new Response($request);
+        $response->code = Response::NOTFOUND;
+        $response->addHeader('Content-Type', $request->mimetypes['html']);
+        $response->body = '<p>404, nothing found</p>';
+        return $response;
+        
+    }
+    
+    /**
+     * Handle a PUT request for this resource by overwriting the resources contents
+     * @param Request request
+     * @return Response
+     */
+    function put($request) {
+        
+        $response = new Response($request);
+        
+        if ($request->data) {
+            
+            $filePath = $this->turnUriIntoFilePath($request->uri, $request);
+            
+            file_put_contents($filePath, $request->data);
+            
+            $response->code = Response::NOCONTENT;
+            
+        } else {
+            
+            $response->code = Response::LENGTHREQUIRED;
+            
+        }
+        
+        return $response;
+        
+    }
+    
+    /**
+     * Handle a DELETE request for this resource by removing the resources file
+     * @param Request request
+     * @return Response
+     */
+    function delete($request) {
+        
+        $response = new Response($request);
+        
+        $filePath = $this->turnUriIntoFilePath($request->uri, $request);
+        
+        if (file_exists($filePath)) {
+            
+            if (unlink($filePath)) {
+                $response->code = Response::NOCONTENT;
+            } else {
+                $response->code = Response::INTERNALSERVERERROR;
+            }
+            
+            return $response;
+            
+        } else { // nothing found, send 404 response
+            $response->code = Response::NOTFOUND;
+        }
+        
+        return $response;
+        
+    }
+    
+}
+
Index: /sandbox/webservice/api/library/tonic/examples/htmlform/htmlform.php
===================================================================
--- /sandbox/webservice/api/library/tonic/examples/htmlform/htmlform.php	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/examples/htmlform/htmlform.php	(revision 6019)
@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * Display and process a HTML form via a HTTP POST request
+ *
+ * This example outputs a simple HTML form and gathers the POSTed data
+ *
+ * @namespace Tonic\Examples\HTMLForm
+ * @uri /htmlform
+ */
+class HTMLForm extends Resource {
+    
+    /**
+     * Handle a GET request for this resource
+     * @param Request request
+     * @return Response
+     */
+    function get($request, $name) {
+        
+        $response = new Response($request);
+        
+        if ($name) {
+            $welcome = "<p>Hello $name.</p>".
+                "<p>Raw POST data:</p>".
+                "<blockquote>$request->data</blockquote>";
+        } else {
+            $welcome = "<p>Enter your name.</p>";
+        }
+        
+        $response->addHeader('Content-type', 'text/html');
+        $response->body = <<<EOF
+<!DOCTYPE html>
+<html>
+    <body>
+        <form action="htmlform" method="post">
+            <label>Name: <input type="text" name="name"></label>
+            <input type="submit">
+        </form>
+        $welcome
+    </body>
+</html>
+EOF;
+        
+        return $response;
+        
+    }
+    
+    /**
+     * Handle a POST request for this resource
+     * @param Request request
+     * @return Response
+     */
+    function post($request) {
+        
+        if (isset($_POST['name'])) {
+            $name = htmlspecialchars($_POST['name']);
+        } else {
+            $name = '';
+        }
+        
+        return $this->get($request, $name);
+        
+    }
+    
+}
+
Index: /sandbox/webservice/api/library/tonic/examples/helloworld/helloworld.php
===================================================================
--- /sandbox/webservice/api/library/tonic/examples/helloworld/helloworld.php	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/examples/helloworld/helloworld.php	(revision 6019)
@@ -0,0 +1,52 @@
+<?php
+
+/**
+ * The Hello World of Tonic
+ *
+ * This example outputs a simple hello world message and the details of the
+ * request and response as generated by Tonic. It also demonstrates etags and
+ * "if none match" functionality.
+ *
+ * @namespace Tonic\Examples\Helloworld
+ * @uri /helloworld
+ */
+class HelloWorldResource extends Resource {
+    
+    /**
+     * Handle a GET request for this resource
+     * @param Request request
+     * @return Response
+     */
+    function get($request) {
+        
+        $response = new Response($request);
+        
+        $etag = md5($request->uri);
+        if ($request->ifNoneMatch($etag)) {
+            
+            $response->code = Response::NOTMODIFIED;
+            
+        } else {
+            
+            $response->code = Response::OK;
+            $response->addHeader('Content-type', 'text/plain');
+            $response->addEtag($etag);
+            $response->body =
+                "Hello world!\n".
+                "\n".
+                "This request:\n".
+                "\n".
+                $request->__toString()."\n".
+                "\n".
+                "This response:\n".
+                "\n".
+                $response->__toString();
+            
+        }
+        
+        return $response;
+        
+    }
+    
+}
+
Index: /sandbox/webservice/api/library/tonic/features/resource.feature
===================================================================
--- /sandbox/webservice/api/library/tonic/features/resource.feature	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/features/resource.feature	(revision 6019)
@@ -0,0 +1,18 @@
+Feature: Resource object
+  In order to execute code
+  As a PHP developer
+  I want a PHP object that represents a HTTP resource
+  
+  Scenario: Load a non existant resource
+    Given the request URI of "/resourcetest"
+    When I create a request object
+    Then I should fail to load the resource with response code "404" and body 'A resource matching URI "/resourcetest" was not found'
+  
+  Scenario: Load a resource that exists
+    Given the request URI of "/resourcetest/one"
+    When I create a request object
+    And I load the resource
+    And execute the request
+    Then the response code should be "200"
+    And the response body should be 'test'
+
Index: /sandbox/webservice/api/library/tonic/features/request.feature
===================================================================
--- /sandbox/webservice/api/library/tonic/features/request.feature	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/features/request.feature	(revision 6019)
@@ -0,0 +1,189 @@
+Feature: HTTP request object
+  In order to handle a request
+  As a PHP developer
+  I want a PHP object that represents the state of the incoming HTTP request
+  
+  Scenario: Have access to the request URI
+    Given the request URI of "/something/otherthing"
+    When I create a request object
+    Then I should see a request URI of "/something/otherthing"
+    
+  Scenario: Query string is ignored
+	Given the request URI of "/something/otherthing?foo=bar"
+	When I create a request object
+    Then I should see a request URI of "/something/otherthing"
+	And I should see the request data ""
+
+  Scenario: Have access to the HTTP request method
+    Given the request method of "GET"
+    When I create a request object
+    Then I should see a request method of "GET"
+
+  Scenario: Have access to the HTTP request method
+    Given the request method of "post"
+    And the request data of "some data"
+    When I create a request object
+    Then I should see a request method of "POST"
+    And I should see the request data "some data"
+    
+  Scenario: Have access to the HTTP request data
+    Given the request method of "PUT"
+    And the request data of "some data"
+    When I create a request object
+    Then I should see a request method of "PUT"
+    And I should see the request data "some data"
+    
+  Scenario: Have bare request URI content negotiated correctly
+    Given the request URI of "/requesttest/one/two"
+    When I create a request object
+    Then I should see a request URI of "/requesttest/one/two"
+    And I should see a negotiated URI of "/requesttest/one/two"
+    And I should see a format negotiated URI of "/requesttest/one/two"
+    And I should see a language negotiated URI of "/requesttest/one/two"
+    
+  Scenario: Have extension request URI content negotiated correctly
+    Given the request URI of "/requesttest/one/two.html"
+    When I create a request object
+    Then I should see a request URI of "/requesttest/one/two"
+    And I should see a negotiated URI of "/requesttest/one/two.html,/requesttest/one/two"
+    And I should see a format negotiated URI of "/requesttest/one/two.html,/requesttest/one/two"
+    And I should see a language negotiated URI of "/requesttest/one/two.html,/requesttest/one/two"
+    
+  Scenario: Have bare request URI with accept header content negotiated correctly
+    Given the request URI of "/requesttest/one/two"
+    And the accept header of "image/png;q=0.5,text/html"
+    When I create a request object
+    Then I should see a request URI of "/requesttest/one/two"
+    And I should see a negotiated URI of "/requesttest/one/two.html,/requesttest/one/two.png,/requesttest/one/two"
+    And I should see a format negotiated URI of "/requesttest/one/two.html,/requesttest/one/two.png,/requesttest/one/two"
+    And I should see a language negotiated URI of "/requesttest/one/two"
+    
+  Scenario: Have extension request URI with accept header content negotiated correctly
+    Given the request URI of "/requesttest/one/two.html"
+    And the accept header of "image/png;q=0.5,text/html"
+    When I create a request object
+    Then I should see a request URI of "/requesttest/one/two"
+    And I should see a negotiated URI of "/requesttest/one/two.html,/requesttest/one/two.png.html,/requesttest/one/two.png,/requesttest/one/two"
+    And I should see a format negotiated URI of "/requesttest/one/two.html,/requesttest/one/two.png,/requesttest/one/two"
+    And I should see a language negotiated URI of "/requesttest/one/two.html,/requesttest/one/two"
+    
+  Scenario: Have bare request URI with language accept header content negotiated correctly
+    Given the request URI of "/requesttest/one/two"
+    And the language accept header of "fr;q=0.5,en"
+    When I create a request object
+    Then I should see a request URI of "/requesttest/one/two"
+    And I should see a negotiated URI of "/requesttest/one/two.en,/requesttest/one/two.fr,/requesttest/one/two"
+    And I should see a format negotiated URI of "/requesttest/one/two"
+    And I should see a language negotiated URI of "/requesttest/one/two.en,/requesttest/one/two.fr,/requesttest/one/two"
+    
+  Scenario: Have extension request URI with language accept header content negotiated correctly
+    Given the request URI of "/requesttest/one/two.html"
+    And the language accept header of "fr;q=0.5,en"
+    When I create a request object
+    Then I should see a request URI of "/requesttest/one/two"
+    And I should see a negotiated URI of "/requesttest/one/two.html.en,/requesttest/one/two.html.fr,/requesttest/one/two.html,/requesttest/one/two.en,/requesttest/one/two.fr,/requesttest/one/two"
+    And I should see a format negotiated URI of "/requesttest/one/two.html,/requesttest/one/two"
+    And I should see a language negotiated URI of "/requesttest/one/two.html,/requesttest/one/two.en,/requesttest/one/two.fr,/requesttest/one/two"
+    
+  Scenario: Have bare request URI with accept and language header content negotiated correctly
+    Given the request URI of "/requesttest/one/two"
+    And the accept header of "image/png;q=0.5,text/html"
+    And the language accept header of "fr;q=0.5,en"
+    When I create a request object
+    Then I should see a request URI of "/requesttest/one/two"
+    And I should see a negotiated URI of "/requesttest/one/two.html.en,/requesttest/one/two.html.fr,/requesttest/one/two.html,/requesttest/one/two.png.en,/requesttest/one/two.png.fr,/requesttest/one/two.png,/requesttest/one/two.en,/requesttest/one/two.fr,/requesttest/one/two"
+    And I should see a format negotiated URI of "/requesttest/one/two.html,/requesttest/one/two.png,/requesttest/one/two"
+    And I should see a language negotiated URI of "/requesttest/one/two.en,/requesttest/one/two.fr,/requesttest/one/two"
+    
+  Scenario: Have extension request URI with accept and language header content negotiated correctly
+    Given the request URI of "/requesttest/one/two.html"
+    And the accept header of "image/png;q=0.5,text/html"
+    And the language accept header of "fr;q=0.5,en"
+    When I create a request object
+    Then I should see a request URI of "/requesttest/one/two"
+    And I should see a negotiated URI of "/requesttest/one/two.html.en,/requesttest/one/two.html.fr,/requesttest/one/two.html,/requesttest/one/two.png.html,/requesttest/one/two.png.en,/requesttest/one/two.png.fr,/requesttest/one/two.png,/requesttest/one/two.en,/requesttest/one/two.fr,/requesttest/one/two"
+    And I should see a format negotiated URI of "/requesttest/one/two.html,/requesttest/one/two.png,/requesttest/one/two"
+    And I should see a language negotiated URI of "/requesttest/one/two.html,/requesttest/one/two.en,/requesttest/one/two.fr,/requesttest/one/two"
+    
+  Scenario: Have if match header
+    Given an if match header of '123123'
+    When I create a request object
+    Then I should see an if match header of "123123"
+    And if match should match "123123"
+    
+  Scenario: Have if match header in quotes
+    Given an if match header of '"123123"'
+    When I create a request object
+    Then I should see an if match header of "123123"
+    And if match should match "123123"
+    
+  Scenario: Have multiple if match headers in quotes
+    Given an if match header of '"123123","456456"'
+    When I create a request object
+    Then I should see an if match header of "123123,456456"
+    And if match should match "123123"
+    And if match should match "456456"
+    
+  Scenario: Have star if match header
+    Given an if match header of '*'
+    When I create a request object
+    Then I should see an if match header of "*"
+    And if match should match "123123"
+    And if match should match "123456"
+    
+  Scenario: Have if none match header
+    Given an if none match header of '123123'
+    When I create a request object
+    Then I should see an if none match header of "123123"
+    And if none match should match "123123"
+    
+  Scenario: Have multiple if none match headers in quotes
+    Given an if none match header of '"123123","456456"'
+    When I create a request object
+    Then I should see an if none match header of "123123,456456"
+    And if none match should match "123123"
+    And if none match should match "456456"
+  
+  Scenario: Have star if none match header
+    Given an if none match header of '*'
+    When I create a request object
+    Then I should see an if none match header of "*"
+    And if none match should not match "123123"
+    And if none match should not match "123456"
+    
+  Scenario: Load a non-existent resource
+    Given the request URI of "/three"
+    When I create a request object
+    Then I should fail to load the resource
+    
+  Scenario: Load a resource
+    Given the request URI of "/requesttest/one"
+    When I create a request object
+    And I load the resource
+    Then I should have a response of type "NewResource"
+    
+  Scenario: Load a child resource
+    Given the request URI of "/requesttest/one/two"
+    When I create a request object
+    And I load the resource
+    Then I should have a response of type "ChildResource"
+    
+  Scenario: Load a resource with a regex match URI
+    Given the request URI of "/requesttest/three/something/four"
+    When I create a request object
+    And I load the resource
+    Then I should have a response of type "NewResource"
+  
+  Scenario: Resource data loading
+    Given the request URI of "/requesttest/one/two"
+    When I create a request object
+    Then I should see resource "namespace" metadata of "Tonic\Tests"
+    And I should see resource "class" metadata of "ChildResource"
+    And I should see resource "priority" metadata of "0"
+    
+  Scenario: Mounting in a namespace to a URI
+    Given the request URI of "/foo/bar/requesttest/one"
+    And a mounting of "Tonic\Tests" to "/foo/bar"
+    When I create a request object
+    And I load the resource
+    Then I should have a response of type "NewResource"
Index: /sandbox/webservice/api/library/tonic/features/response.feature
===================================================================
--- /sandbox/webservice/api/library/tonic/features/response.feature	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/features/response.feature	(revision 6019)
@@ -0,0 +1,58 @@
+Feature: Response object
+  In order to output a response representation
+  As a PHP developer
+  I want a PHP object that represents a HTTP response
+  
+#  Scenario: Output should be gzipped if the client supports it
+#    Given the request URI of "/resourcetest/one"
+#    And an accept encoding of "gzip"
+#    When I create a request object
+#    And I load the resource
+#    And execute the request
+#    And I process content encoding
+#    Then the response header "Content-Encoding" should contain 'gzip'
+#    And the response body should be gzipped and be "test"
+#
+#  Scenario: Output should be deflated if the client supports it
+#    Given the request URI of "/resourcetest/one"
+#    And an accept encoding of "deflate"
+#    When I create a request object
+#    And I load the resource
+#    And execute the request
+#    And I process content encoding
+#    Then the response header "Content-Encoding" should contain 'deflate'
+#    And the response body should be deflated and be "test"
+#
+#  Scenario: Output should be compress if the client supports it
+#    Given the request URI of "/resourcetest/one"
+#    And an accept encoding of "compress"
+#    When I create a request object
+#    And I load the resource
+#    And execute the request
+#    And I process content encoding
+#    Then the response header "Content-Encoding" should contain 'compress'
+#    And the response body should be compressed and be "test"
+
+  Scenario: Responses should have a default cache header
+    Given the request URI of "/resourcetest/one"
+    When I create a request object
+    And I load the resource
+    And execute the request
+    And I add a cache header of ""
+    Then the response header "Cache-Control" should contain 'max-age=86400, must-revalidate'
+
+  Scenario: Responses told not to cache should send a zero cache header
+    Given the request URI of "/resourcetest/one"
+    When I create a request object
+    And I load the resource
+    And execute the request
+    And I add a cache header of "0"
+    Then the response header "Cache-Control" should contain 'no-cache'
+
+  Scenario: Responses given an etag should send an etag response header
+    Given the request URI of "/resourcetest/one"
+    When I create a request object
+    And I load the resource
+    And execute the request
+    And I add an etag header of "123123"
+    Then the response header "Etag" should contain '"123123"'
Index: /sandbox/webservice/api/library/tonic/features/bootstrap/FeatureContext.php
===================================================================
--- /sandbox/webservice/api/library/tonic/features/bootstrap/FeatureContext.php	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/features/bootstrap/FeatureContext.php	(revision 6019)
@@ -0,0 +1,342 @@
+<?php
+
+use Behat\Behat\Context\ClosuredContextInterface,
+    Behat\Behat\Context\TranslatedContextInterface,
+    Behat\Behat\Context\BehatContext,
+    Behat\Behat\Exception\PendingException;
+use Behat\Gherkin\Node\PyStringNode,
+    Behat\Gherkin\Node\TableNode;
+
+/**
+ * Features context.
+ */
+class FeatureContext extends BehatContext
+{
+    /**
+     * Initializes context.
+     * Every scenario gets it's own context object.
+     *
+     * @param   array   $parameters     context parameters (set them up through behat.yml)
+     */
+    public function __construct(array $parameters)
+    {
+        // Initialize your context here
+		$this->config = array();
+    }
+
+    /**
+     * @Given /^the request URI of "([^"]*)"$/
+     */
+    public function theRequestUriOf($uri)
+    {
+		$this->config['uri'] = $uri;
+    }
+
+    /**
+     * @When /^I create a request object$/
+     */
+    public function iCreateARequestObject()
+    {
+		$this->request = new Request($this->config);
+    }
+
+    /**
+     * @Then /^I should see a request URI of "([^"]*)"$/
+     */
+    public function iShouldSeeARequestUriOf($uri)
+    {
+		if ($this->request->uri != $uri)
+			throw new Exception;
+    }
+
+    /**
+     * @Given /^the request method of "([^"]*)"$/
+     */
+    public function theRequestMethodOf($method)
+    {
+		$this->config['method'] = $method;
+    }
+
+    /**
+     * @Then /^I should see a request method of "([^"]*)"$/
+     */
+    public function iShouldSeeARequestMethodOf($method)
+    {
+		if ($this->request->method != $method)
+			throw new Exception;
+    }
+
+    /**
+     * @Given /^the request data of "([^"]*)"$/
+     */
+    public function theRequestDataOf($data)
+    {
+		$this->config['data'] = $data;
+    }
+
+    /**
+     * @Given /^I should see the request data "([^"]*)"$/
+     */
+    public function iShouldSeeTheRequestData($data)
+    {
+		if($this->request->data != $data)
+			throw new Exception;
+    }
+
+    /**
+     * @Then /^I should see a negotiated URI of "([^"]*)"$/
+     */
+    public function iShouldSeeANegotiatedUriOf($uri)
+    {
+		if($this->request->negotiatedUris != explode(',', $uri))
+			throw new Exception;
+    }
+
+    /**
+     * @Then /^I should see a format negotiated URI of "([^"]*)"$/
+     */
+    public function iShouldSeeAFormatNegotiatedUriOf($uri)
+    {
+		if($this->request->formatNegotiatedUris != explode(',', $uri))
+			throw new Exception;
+    }
+
+    /**
+     * @Then /^I should see a language negotiated URI of "([^"]*)"$/
+     */
+    public function iShouldSeeALanguageNegotiatedUriOf($uri)
+    {
+		if($this->request->languageNegotiatedUris != explode(',', $uri))
+			throw new Exception;
+    }
+
+    /**
+     * @Given /^the accept header of "([^"]*)"$/
+     */
+    public function theAcceptHeaderOf($header)
+    {
+		$this->config['accept'] = $header;
+    }
+
+    /**
+     * @Given /^the language accept header of "([^"]*)"$/
+     */
+    public function theLanguageAcceptHeaderOf($header)
+    {
+		$this->config['acceptLang'] = $header;
+    }
+
+    /**
+     * @Given /^an if match header of '([^']*)'$/
+     */
+    public function anIfMatchHeaderOf($header)
+    {
+		$this->config['ifMatch'] = $header;
+    }
+
+    /**
+     * @Then /^I should see an if match header of "([^"]*)"$/
+     */
+    public function iShouldSeeAnIfMatchHeaderOf($header)
+    {
+		if($this->request->ifMatch != explode(',', $header))
+			throw new Exception;
+    }
+
+    /**
+     * @Then /^if match should match "([^"]*)"$/
+     */
+    public function ifMatchShouldMatch($match)
+    {
+		if(!$this->request->ifMatch($match))
+			throw new Exception;
+    }
+
+    /**
+     * @Given /^an if none match header of '([^']*)'$/
+     */
+    public function anIfNoneMatchHeaderOf($header)
+    {
+		$this->config['ifNoneMatch'] = $header;
+    }
+
+    /**
+     * @Then /^I should see an if none match header of "([^"]*)"$/
+     */
+    public function iShouldSeeAnIfNoneMatchHeaderOf($header)
+    {
+		if($this->request->ifNoneMatch != explode(',', $header))
+			throw new Exception;
+    }
+
+    /**
+     * @Then /^if none match should match "([^"]*)"$/
+     */
+    public function ifNoneMatchShouldMatch($match)
+    {
+		if(!$this->request->ifNoneMatch($match))
+			throw new Exception;
+    }
+
+    /**
+     * @Then /^if none match should not match "([^"]*)"$/
+     */
+    public function ifNoneMatchShouldNotMatch($match)
+    {
+		if($this->request->ifNoneMatch($match))
+			throw new Exception;
+    }
+
+    /**
+     * @Given /^I load the resource$/
+     */
+    public function iLoadTheResource()
+    {
+		$this->resource = $this->request->loadResource();
+    }
+
+    /**
+     * @Then /^I should fail to load the resource$/
+     */
+    public function iShouldFailToLoadTheResource()
+    {
+		try {
+			$this->request->loadResource();
+		} catch(ResponseException $e) {
+			if($e->getCode() != Response::NOTFOUND)
+				throw new Exception;
+		}
+    }
+
+    /**
+     * @Then /^I should have a response of type "([^"]*)"$/
+     */
+    public function iShouldHaveAResponseOfType($type)
+    {
+		if(get_class($this->resource != $type))
+			throw new Exception;
+    }
+
+    /**
+     * @Then /^I should see resource "([^"]*)" metadata of "([^"]*)"$/
+     */
+    public function iShouldSeeResourceMetadataOf($argument1, $argument2)
+    {
+		if($this->request->resources[$this->request->uri][$argument1] != $argument2)
+			throw new Exception("metadata $argument1: want $argument2, got {$this->request->resources[$this->request->uri][$argument1]}");
+    }
+
+    /**
+     * @Given /^a mounting of "([^"]*)" to "([^"]*)"$/
+     */
+    public function aMountingOfTo($argument1, $argument2)
+    {
+		$this->config['mount'][$argument1] = $argument2;
+    }
+
+    /**
+     * @Given /^execute the request$/
+     */
+    public function executeTheRequest()
+    {
+    	$this->response = $this->resource->exec($this->request);
+    }
+
+    /**
+     * @Given /^an accept encoding of "([^"]*)"$/
+     */
+    public function anAcceptEncodingOf($encoding)
+    {
+		$this->config['acceptEncoding'] = $encoding;
+    }
+
+#    /**
+#     * @Given /^I process content encoding$/
+#     */
+#    public function iProcessContentEncoding()
+#    {
+#		$this->response->doContentEncoding();
+#    }
+
+    /**
+     * @Then /^the response header "([^"]*)" should contain '([^']*)'$/
+     */
+    public function theResponseHeaderShouldContain($header, $contents)
+    {
+		if($this->response->headers[$header] != $contents)
+			throw new Exception;
+    }
+
+#    /**
+#     * @Then /^the response body should be ([^ ]*) and be "([^"]*)"$/
+#     */
+#    public function theResponseBodyShouldBeTransformedAndBe($transform, $contents)
+#    {
+#    	switch ($transform) {
+#    	case 'gzipped':
+##    	    var_dump($this->response->body, $contents, gzencode($contents));
+#    	    if ($this->response->body != gzencode($contents)) throw new Exception;
+#    	    break;
+#    	case 'deflated':
+#    	    if ($this->response->body != gzdeflate($contents)) throw new Exception;
+#    	    break;
+#    	case 'compressed':
+#    	    if ($this->response->body != gzcompress($contents)) throw new Exception;
+#    	    break;
+#    	}
+#    }
+
+    /**
+     * @Given /^I add a cache header of "([^"]*)"$/
+     */
+    public function iAddACacheHeaderOf($header)
+	{
+		if($header == '') {
+			$this->response->addCacheHeader();
+		} else {
+			$this->response->addCacheHeader($header);
+		}
+	}
+
+    /**
+     * @Given /^I add an etag header of "([^"]*)"$/
+     */
+    public function iAddAnEtagHeaderOf($etag)
+    {
+		$this->response->addEtag($etag);
+    }
+
+    /**
+     * @Then /^I should fail to load the resource with response code "([^"]*)" and body '([^']*)'$/
+     */
+    public function iShouldFailToLoadTheResourceWithResponseCodeAndBody($code, $body)
+    {
+		try {
+			$this->request->loadResource();
+		} catch(Exception $e) {
+			if($e->getCode() != $code)
+				throw new Exception('bad code ' . $e->getCode() . " (want $code)");
+			if($e->getMessage() != $body)
+				throw new Exception('bad error message ' . $e->getMessage() . " (want $body)");
+		}
+    }
+
+    /**
+     * @Then /^the response code should be "([^"]*)"$/
+     */
+    public function theResponseCodeShouldBe($code)
+    {
+		if($this->response->code != $code)
+			throw new Exception;
+    }
+
+    /**
+     * @Given /^the response body should be '([^']*)'$/
+     */
+    public function theResponseBodyShouldBe($body)
+    {
+		if($this->response->body != $body)
+			throw new Exception;
+    }
+
+}
Index: /sandbox/webservice/api/library/tonic/features/bootstrap/env.php
===================================================================
--- /sandbox/webservice/api/library/tonic/features/bootstrap/env.php	(revision 6019)
+++ /sandbox/webservice/api/library/tonic/features/bootstrap/env.php	(revision 6019)
@@ -0,0 +1,50 @@
+<?php
+
+require_once __DIR__.'/../../lib/tonic.php';
+
+/* Test resource definitions */
+
+if (!class_exists('NewResource')) {
+    
+    /**
+     * @namespace Tonic\Tests
+     * @uri /requesttest/one
+     * @uri /requesttest/three/.+/four 12
+     */
+    class NewResource extends Resource {
+    
+    }
+
+}
+
+if (!class_exists('ChildResource')) {
+
+    /**
+     * @namespace Tonic\Tests
+     * @uri /requesttest/one/two
+     */
+    class ChildResource extends NewResource {
+    
+    }
+
+}
+
+if (!class_exists('TestResource')) {
+    
+    /**
+     * @namespace Tonic\Tests
+     * @uri /resourcetest/one
+     */
+    class TestResource extends Resource {
+        
+        function get($request) {
+            
+            $response = new Response($request);
+            $response->body = 'test';
+            return $response;
+            
+        }
+        
+    }
+
+}
Index: /sandbox/webservice/api/config/Tonic.srv
===================================================================
--- /sandbox/webservice/api/config/Tonic.srv	(revision 6019)
+++ /sandbox/webservice/api/config/Tonic.srv	(revision 6019)
@@ -0,0 +1,20 @@
+[/ExpressoVersion]
+ExpressoVersionResource = /core/ExpressoVersionResource.php
+
+[/Login]
+LoginResource = /core/LoginResource.php
+
+[/Logout]
+LogoutResource = /core/LogoutResource.php
+
+[/Mail/Folders]
+FoldersResource = /mail/FoldersResource.php
+
+[/Mail/Messages]
+MessagesResource = /mail/MessagesResource.php
+
+[/Catalog/Contacts]
+ContactsResource = /catalog/ContactsResource.php
+
+[/Catalog/ContactPicture]
+ContactPictureResource = /catalog/ContactPictureResource.php  
Index: /sandbox/webservice/api/rest/mail/MessagesResource.php
===================================================================
--- /sandbox/webservice/api/rest/mail/MessagesResource.php	(revision 6019)
+++ /sandbox/webservice/api/rest/mail/MessagesResource.php	(revision 6019)
@@ -0,0 +1,95 @@
+<?php
+
+class MessagesResource extends Mail {	
+	public function post($request){
+		// to Receive POST Params (use $this->params)
+ 		parent::post($request);	
+		
+		if($this-> isLoggedIn()) 
+		{					
+
+			if($this->getParam('folderID') && $this->getParam('msgID') > 0) {
+				$msg = $this->getMessage();
+				$result = array ('messages' => array($msg));
+				$this->setResult($result) ;
+				return $this->getResponse($request);
+			}
+	
+			$max_email_per_page = intval($this->getParam('resultsPerPage') ? $this->getParam('resultsPerPage') :
+					$this->getImap()->prefs['max_email_per_page']);
+	
+			$current_page = intval($this->getParam('page') ? $this->getParam('page') : 1);
+	
+			$msg_range_begin = ($max_email_per_page * ($current_page - 1)) + 1;
+			$msg_range_end = $msg_range_begin + ($max_email_per_page  - 1);
+	
+			$this->getImap()->prefs['preview_msg_subject'] = "1";
+	
+	
+			$imap_msgs = $this->getImap()-> get_range_msgs2(
+					array(	"folder"			=> $this->getParam('folderID'),
+							"msg_range_begin" 	=> $msg_range_begin,
+							"msg_range_end"	 	=> $msg_range_end,
+							"search_box_type"	=> "ALL",
+							"sort_box_reverse"	=> "1",
+							"sort_box_type"		=> "SORTARRIVAL"
+					));
+		}
+		if(!$imap_msgs){
+			return $this->getResponse($request);
+		}
+		
+		$all_msgs = array();
+	
+		foreach($imap_msgs as $i => $imap_msg) {
+			if(!is_int($i)) {
+				continue;
+			}
+			$msg = array();
+			$msg['msgID'] = $imap_msg['msg_number'];
+			$msg['folderID'] =  $imap_msgs['folder'];
+			$msg['msgDate']	= gmdate('d/m/Y H:i', $imap_msg['timestamp']);
+			$msg['msgFrom']['fullName'] = $imap_msg['from']['name'];
+			$msg['msgFrom']['mailAddress'] = $imap_msg['from']['email'];
+			$msg['msgTo'][0]['fullName'] = $imap_msg['to']['name'];
+			$msg['msgTo'][0]['mailAddress'] = $imap_msg['to']['email'];
+			$msg['msgReplyTo'][0] = $this->formatMailObject($imap_msg['reply_toaddress']);
+			$msg['msgSubject']  = mb_convert_encoding($imap_msg['subject'],"UTF8", "ISO_8859-1");
+	
+			$msg['msgHasAttachments'] = $imap_msg['attachment'] ? "1" : "0";
+	
+			$msg['msgFlagged'] 	= $imap_msg['Flagged'] == "F" ? "1" : "0";
+			$msg['msgForwarded']= $imap_msg['Forwarded'] == "F" ? "1" : "0";
+			$msg['msgAnswered'] = $imap_msg['Answered'] == "A" ? "1" : "0";
+			$msg['msgDraft']	= $imap_msg['Draft'] == "X" ? "1" : "0";
+			$msg['msgSeen'] 	= $imap_msg['Unseen'] == "U" ? "0" : "1";
+	
+			$msg['ContentType']	= $imap_msg['ContentType'];
+			$msg['msgSize'] 	= $imap_msg['Size'];
+	
+			$msg['msgBodyResume'] = $imap_msg['msg_sample']['body'];
+	
+			if($this->expressoVersion != "2.2") {
+				$msg['msgBodyResume'] =  base64_decode($msg['msgBodyResume']);
+			}
+	
+			$msg['msgBodyResume'] = substr($msg['msgBodyResume'], 2);
+			$msg['msgBodyResume'] = str_replace("\r\n", "", $msg['msgBodyResume']);
+			$msg['msgBodyResume'] = str_replace(chr(160)," ", $msg['msgBodyResume']);
+			$msg['msgBodyResume'] = preg_replace('/\s\s+/', '', $msg['msgBodyResume']);
+			$msg['msgBodyResume'] = mb_convert_encoding($msg['msgBodyResume'],"UTF8", "ISO_8859-1");
+	
+			$all_msgs[] = $msg;
+		}
+		$result = array (
+				'messages' 	  => $all_msgs,
+				'timeZone'	  => $imap_msgs['offsetToGMT'],
+				'totalUnseen' => $imap_msgs['tot_unseen']
+		);
+			
+		$this->setResult($result);	
+		
+		//	to Send Response (JSON RPC format)
+		return $this->getResponse($request);		
+	}
+}
Index: /sandbox/webservice/api/rest/mail/Mail.php
===================================================================
--- /sandbox/webservice/api/rest/mail/Mail.php	(revision 6019)
+++ /sandbox/webservice/api/rest/mail/Mail.php	(revision 6019)
@@ -0,0 +1,146 @@
+<?php
+
+class Mail extends Expresso {
+	protected $defaultFolders;
+	protected $imap;
+
+	protected function formatMailObject($str) {
+		$str = html_entity_decode($str);
+		$str = preg_replace('/[<>\'"]/', '', $str);
+		$return = array();            
+		if(preg_match('/[[:alnum:]\._\-]+@[[:alnum:]_\-\.]+/',$str, $matches1) &&
+			preg_match('/[[:alnum:]\._\-\ ]+/',$str, $matches2)){            		            
+			return array(					            
+            		'fullName' 	  => mb_convert_encoding(str_replace($matches1[0],'', $str), "UTF8", "ISO_8859-1"),
+					'mailAddress' => $matches1[0]
+					);
+        }
+        else{            	
+           	return array('mailAddress' => $str);           	
+		}		
+    }
+    
+	private function loadLang($lang_user){		
+		$fn = PHPGW_INCLUDE_ROOT."/expressoMail1_2/setup/phpgw_".$lang_user.'.lang';
+		if (file_exists($fn)){
+			$fp = fopen($fn,'r');
+			while ($data = fgets($fp,16000)){
+				list($message_id,$app_name,$null,$content) = explode("\t",substr($data,0,-1));
+				$_SESSION['phpgw_info']['expressomail']['lang'][$message_id] = $content;
+			}
+			fclose($fp);
+		}
+	}
+	
+		
+	protected function getImap(){
+		if($this->imap == null) {
+			$boemailadmin	= CreateObject('emailadmin.bo');
+			$emailadmin_profile = $boemailadmin->getProfileList();
+			$_SESSION['phpgw_info']['expressomail']['email_server'] = $boemailadmin->getProfile($emailadmin_profile[0]['profileID']);
+			
+			$preferences = $GLOBALS['phpgw']->preferences->read();
+			$_SESSION['phpgw_info']['user']['preferences']['expressoMail'] = $preferences['expressoMail'];
+			$_SESSION['phpgw_info']['user']['preferences']['expressoMail']['outoffice'] = $GLOBALS['phpgw_info']['user']['preferences']['expressoMail']['outoffice'];
+			$_SESSION['phpgw_info']['user']['preferences']['expressoMail']['telephone_number'] = $GLOBALS['phpgw_info']['user']['telephonenumber'];
+			$_SESSION['phpgw_info']['user']['preferences']['expressoMail']['use_cache'] = $current_config['expressoMail_enable_cache'];
+			$_SESSION['phpgw_info']['user']['preferences']['expressoMail']['use_x_origin'] = $current_config['expressoMail_use_x_origin'];
+			$_SESSION['phpgw_info']['user']['preferences']['expressoMail']['number_of_contacts'] = $current_config['expressoMail_Number_of_dynamic_contacts'] ? $current_config['expressoMail_Number_of_dynamic_contacts'] : "0";
+			$_SESSION['phpgw_info']['user']['preferences']['expressoMail']['notification_domains'] = $current_config['expressoMail_notification_domains'];
+			$_SESSION['phpgw_info']['user']['preferences']['expressoMail']['search_result_number'] = $GLOBALS['phpgw_info']['user']['preferences']['expressoMail']['search_result_number'] ? $GLOBALS['phpgw_info']['user']['preferences']['expressoMail']['search_result_number'] : "50";
+			$_SESSION['phpgw_info']['user']['preferences']['expressoMail']['search_characters_number'] = $GLOBALS['phpgw_info']['user']['preferences']['expressoMail']['search_characters_number'] ? $GLOBALS['phpgw_info']['user']['preferences']['expressoMail']['search_characters_number'] : "4";			
+			$_SESSION['phpgw_info']['user']['preferences']['expressoMail']['max_attachment_size'] = $current_config['expressoMail_Max_attachment_size'] ? $current_config['expressoMail_Max_attachment_size']."M" : '';
+			$_SESSION['phpgw_info']['user']['preferences']['expressoMail']['max_msg_size'] = $GLOBALS['phpgw_info']['user']['preferences']['expressoMail']['max_msg_size'] ? $GLOBALS['phpgw_info']['user']['preferences']['expressoMail']['max_msg_size'] : "0";
+			$_SESSION['phpgw_info']['user']['preferences']['expressoMail']['imap_max_folders'] = $current_config['expressoMail_imap_max_folders'];
+			$_SESSION['phpgw_info']['user']['preferences']['expressoMail']['max_email_per_page'] = $GLOBALS['phpgw_info']['user']['preferences']['expressoMail']['max_email_per_page'] ? $GLOBALS['phpgw_info']['user']['preferences']['expressoMail']['max_email_per_page'] : "50";
+			$_SESSION['phpgw_info']['user']['preferences']['expressoMail']['extended_info'] = $GLOBALS['phpgw_info']['user']['preferences']['expressoMail']['extended_info']?$_SESSION['phpgw_info']['user']['preferences']['expressoMail']['extended_info'] = $GLOBALS['phpgw_info']['user']['preferences']['expressoMail']['extended_info']:'0';
+			$_SESSION['phpgw_info']['user']['preferences']['expressoMail']['from_to_sent'] = $GLOBALS['phpgw_info']['user']['preferences']['expressoMail']['from_to_sent'] ? $GLOBALS['phpgw_info']['user']['preferences']['expressoMail']['from_to_sent'] : "0";
+			$_SESSION['phpgw_info']['user']['preferences']['expressoMail']['return_recipient_deafault'] = $GLOBALS['phpgw_info']['user']['preferences']['expressoMail']['return_recipient_deafault'] ? $GLOBALS['phpgw_info']['user']['preferences']['expressoMail']['return_recipient_deafault'] : "0";			
+			$_SESSION['phpgw_info']['user']['preferences']['expressoMail']['quick_search_default'] = $GLOBALS['phpgw_info']['user']['preferences']['expressoMail']['quick_search_default'] ? $GLOBALS['phpgw_info']['user']['preferences']['expressoMail']['quick_search_default'] : 1;
+			
+			
+			$this->loadLang( $GLOBALS['phpgw_info']['user']['preferences']['common']['lang']);														
+			$_SESSION['phpgw_info']['expressomail']['user']['userid'] = $GLOBALS['phpgw_info']['user']['userid'];
+			$_SESSION['phpgw_info']['expressomail']['user']['passwd'] = $GLOBALS['phpgw_info']['user']['passwd'];
+			
+			
+			$this->imap = CreateObject("expressoMail1_2.imap_functions");
+			
+			if($this->defaultFolders == null) {				
+				$sent = empty($_SESSION['phpgw_info']['expressomail']['email_server']['imapDefaultSentFolder']) ?
+					$this->imap->functions->getLang("Sent") : $_SESSION['phpgw_info']['expressomail']['email_server']['imapDefaultSentFolder'];
+				$spam = empty($_SESSION['phpgw_info']['expressomail']['email_server']['imapDefaultSpamFolder'])	?
+					$this->imap->functions->getLang("Spam"): $_SESSION['phpgw_info']['expressomail']['email_server']['imapDefaultSpamFolder'];
+				$drafts = empty($_SESSION['phpgw_info']['expressomail']['email_server']['imapDefaultDraftsFolder']) ?
+					$this->imap->functions->getLang("Drafts") : $_SESSION['phpgw_info']['expressomail']['email_server']['imapDefaultDraftsFolder'];
+				$trash = empty($_SESSION['phpgw_info']['expressomail']['email_server']['imapDefaultTrashFolder']) ?
+					$this->imap->functions->getLang("Trash") : $_SESSION['phpgw_info']['expressomail']['email_server']['imapDefaultTrashFolder'];
+				
+				$this->defaultFolders = array (
+						'INBOX' => 0,
+						'INBOX'.$this->imap->imap_delimiter.$spam   => 2,
+						'INBOX'.$this->imap->imap_delimiter.$sent   => 1,
+						'INBOX'.$this->imap->imap_delimiter.$drafts => 4,
+						'INBOX'.$this->imap->imap_delimiter.$trash   => 3
+					);					
+			}
+		}
+				
+		return $this->imap;
+	}
+		
+		
+	
+	protected function getMessage(){				
+		$info_msg = $this -> getImap()-> get_info_msg(
+				array( 'msg_folder'	 => urlencode($this->getParam('folderID')),
+						'msg_number' => $this->getParam('msgID')				
+				));
+		
+		$msg['msgID']    = $info_msg['msg_number'];
+		$msg['folderID'] = $info_msg['msg_folder'];
+		//$msg['msgDate']	 =  $info_msg['fulldate'];
+		$msg['msgDate']  =  $info_msg['msg_day']." ".$info_msg['msg_hour'];
+
+		$msg['msgFrom']['fullName'] 	= $info_msg['from']['name'];
+		$msg['msgFrom']['mailAddress'] 	= $info_msg['from']['email'];		
+		$msg['msgSender']['fullName'] 	= $info_msg['sender']['name'];
+		$msg['msgSender']['mailAddress']= $info_msg['sender']['email'];
+		
+		$toaddresses = explode(",",$info_msg['toaddress2']);
+		if(count($toaddresses) > 1) {				
+			foreach ($toaddresses as $i => $toaddress){
+				$msg['msgTo'][$i] = $this->formatMailObject($toaddress);			
+			}
+		}
+		else{
+			$msg['msgTo'][0] = $this->formatMailObject($info_msg['toaddress2']);
+		}
+			
+		$ccaddresses = explode(",",$info_msg['cc']);
+		if(count($ccaddresses) > 1) {		
+			foreach ($ccaddresses as $i => $ccaddress){
+				$msg['msgCC'][$i] = $this->formatMailObject($ccaddress);
+			}
+		}
+		else{
+			$msg['msgCC'][0] = $this->formatMailObject($info_msg['cc']);
+		}
+		
+
+		$msg['msgReplyTo'][0] = $this->formatMailObject($info_msg['reply_toaddress']);						
+		$msg['msgSubject']  = mb_convert_encoding($info_msg['subject'],"UTF8", "ISO_8859-1");		
+		$msg['msgHasAttachments'] = $info_msg['attachments'] ? "1" : "0";		
+		$msg['msgAttachments'] = $info_msg['attachments'];		
+		$msg['msgFlagged'] 	= $info_msg['Flagged'] == "F" ? "1" : "0";
+		$msg['msgForwarded']= $info_msg['Forwarded'] == "F" ? "1" : "0";
+		$msg['msgAnswered'] = $info_msg['Answered'] == "A" ? "1" : "0";
+		$msg['msgDraft']	= $info_msg['Draft'] == "X" ? "1" : "0";
+		$msg['msgSeen'] 	= $info_msg['Unseen'] == "U" ? "0" : "1";				
+		$msg['msgSize'] 	= $info_msg['Size'];
+		$msg['msgBody']     = $info_msg['body'];		
+		
+		return $msg;		
+	}
+		
+}
Index: /sandbox/webservice/api/rest/mail/FoldersResource.php
===================================================================
--- /sandbox/webservice/api/rest/mail/FoldersResource.php	(revision 6019)
+++ /sandbox/webservice/api/rest/mail/FoldersResource.php	(revision 6019)
@@ -0,0 +1,79 @@
+<?php
+
+class FoldersResource extends Mail {	
+	public function post($request){
+		// to Receive POST Params (use $this->params)		
+ 		parent::post($request);	 		
+ 		
+		if($this-> isLoggedIn()) 
+		{	
+			$imap_folders =  $this->getImap()->get_folders_list();
+		}
+		if(!$imap_folders){
+			return $this->getResponse($request);
+		}
+			
+		$all_folders    = array();
+		$user_folders   = array();
+		$shared_folders = array();
+		
+		foreach ($imap_folders as $i => $imap_folder) {
+			if(is_int($i)) {
+				$folder = array();
+				$folder['folderName'] 	  = mb_convert_encoding($imap_folder['folder_name'], "UTF8", "ISO_8859-1");
+				if($folder['folderName'] == 'INBOX') {
+					$folder['folderName'] = $this->getImap()->functions->getLang("Inbox");
+				}
+				$folder['folderParentID'] = mb_convert_encoding($imap_folder['folder_parent'],'UTF8','ISO-8859-1');
+				$folder['folderHasChildren'] = $imap_folder['folder_hasChildren'];
+				$folder['qtdUnreadMessages'] = $imap_folder['folder_unseen'];
+				$folder['qtdMessages'] = $this->getImap()->get_num_msgs(array("folder" => $imap_folder['folder_id']));
+				$folder['folderID'] = mb_convert_encoding($imap_folder['folder_id'],'UTF8','ISO-8859-1');
+		
+				if(substr($folder['folderID'], 0, 4) == 'user'){
+					$folder['folderType'] = "6";
+					$shared_folders[] = $folder;
+				}else if(array_key_exists($folder['folderID'], $this->defaultFolders) !== false) {
+					$folder_type = $this->defaultFolders[$folder['folderID']];
+					$folder['folderType'] = $folder_type;
+					$default_folders[] = $folder;
+				}
+				else{
+					$folder['folderType'] = "5";
+					$user_folders[] = $folder;
+				}
+			}
+		}
+		
+		$all_folders = array_merge($default_folders, $user_folders, $shared_folders);
+		$quota_folders = $this->getImap()->get_quota_folders();
+		$search = $this->getParam('search') ? mb_convert_encoding($this->getParam('search'),"ISO_8859-1", "UTF8") : null;
+		foreach($all_folders as $i => $folder){
+			$folder_name = mb_convert_encoding($folder['folderName'],"ISO_8859-1", "UTF8");
+			$folder_id   = mb_convert_encoding($folder['folderID'],"ISO_8859-1", "UTF8");
+			if($folder_id == 'INBOX') {
+				$j = $this->getImap()->functions->getLang("Inbox");
+			}else {
+				$j = str_replace("INBOX".$this->getImap()->imap_delimiter,"",$folder_id);
+			}
+			$all_folders[$i]['diskSizeUsed'] 	= $quota_folders[$j]['quota_used'];
+			$all_folders[$i]['diskSizePercent'] = $quota_folders[$j]['quota_percent']/100;
+			if($search != null && stristr($folder_name, $search) == null){
+				unset($all_folders[$i]);
+			}
+		}
+		
+		$result = array (
+				'folders' => array_values($all_folders),
+				'diskSizeUsed'     => $quota_folders['quota_root']['quota_used']*1024,
+				'diskSizeLimit'    => $quota_folders['quota_root']['quota_limit']*1024,
+				'diskSizePercent'  => $quota_folders['quota_root']['quota_percent']/100
+		);			
+		
+		$this->setResult($result);
+		
+		//to Send Response (JSON RPC format)
+		return $this->getResponse($request);		
+	}	
+
+}
Index: /sandbox/webservice/api/rest/dispatch.php
===================================================================
--- /sandbox/webservice/api/rest/dispatch.php	(revision 6019)
+++ /sandbox/webservice/api/rest/dispatch.php	(revision 6019)
@@ -0,0 +1,62 @@
+<?php
+
+// load Tonic library
+require_once __DIR__.'/../library/tonic/lib/tonic.php';
+
+// load Expresso classes
+require_once("./core/Errors.php");
+require_once("./core/Expresso.php");
+
+// Load all resources
+require_once("./core/ExpressoVersionResource.php");
+require_once("./core/LoginResource.php");
+require_once("./core/LogoutResource.php");
+
+require_once("./mail/Mail.php");
+require_once("./mail/FoldersResource.php");
+require_once("./mail/MessagesResource.php");
+
+require_once("./catalog/Catalog.php");
+require_once("./catalog/ContactsResource.php");
+require_once("./catalog/ContactPictureResource.php");
+
+//Retrieveing the mapping of the URIs and his respectives classNames and classPath
+$config = parse_ini_file( __DIR__ . '/../config/Tonic.srv', true );
+$autoload = array();
+$classpath = array();
+
+foreach( $config as $uri => $classFile )
+	foreach( $classFile as $className => $filePath )
+	{
+		$autoload[ $uri ] = $className;
+		$classpath[ $className ] = $filePath;
+	}
+
+	$request = new Request(array(
+			'baseUri' => dirname($_SERVER['SCRIPT_NAME']),
+			'autoload' => $autoload
+	));
+	
+try {
+	$resource = $request->loadResource();
+	$response = $resource->exec($request);
+	
+} catch (ResponseException $e) {
+	
+	switch ($e->getCode()) {
+		
+		case Response::UNAUTHORIZED:
+			$response = $e->response($request);
+			$response->addHeader('WWW-Authenticate', 'Basic realm="Tonic"');
+			break;
+		default:
+			$expresso = new Expresso($request->id);
+			$expresso->post($request);
+			$expresso->setError($e->getCode(), $request->uri);			
+			$response = $expresso->getResponse();			
+	}
+}
+
+$response->output();
+
+?>
Index: /sandbox/webservice/api/rest/core/Errors.php
===================================================================
--- /sandbox/webservice/api/rest/core/Errors.php	(revision 6019)
+++ /sandbox/webservice/api/rest/core/Errors.php	(revision 6019)
@@ -0,0 +1,73 @@
+<?php
+
+final class Errors {
+		
+	// Errors from -32000 to -32099 => Server error	:Reserved for implementation-defined server-errors.
+	const E_PARSE_ERROR 		= "-32700";	
+	const E_INVALID_REQUEST 	= "-32600";
+	const E_METHOD_NOT_FOUND 	= "-32601";
+	const E_INVALID_PARAMS 		= "-32602";
+	const E_INTERNAL_ERROR 		= "-32603";	
+
+	const E_HTTP_NOT_FOUND		= "404";
+	const E_UNKNOWN_ERROR		= "500";
+	
+	
+	// Errors inherited from Expresso Application.	
+	const LOGIN_SUCCESS_LOGOUT 			= "1";
+	const LOGIN_SESSION_EXPIRED 		= "2";
+	const LOGIN_NOT_LOGGED_IN 			= "3";
+	const LOGIN_COOKIES_REQUIRED 		= "4";
+	const LOGIN_BADLOGIN 				= "5";
+	const LOGIN_PASSWORD_EXPIRED 		= "6";
+	const LOGIN_AUTH_INVALID 			= "7";
+	const LOGIN_SESSION_NOT_VERIFIED 	= "10";
+	const LOGIN_ACCOUNT_EXPIRED 		= "98";
+	const LOGIN_LOGIN_BLOCKED 			= "99";
+	const LOGIN_INVALID_LOGIN 			= "200";
+	
+
+	const CATALOG_MIN_ARGUMENT_SEARCH	= "1001";	
+	
+	static private $reservedErrors = array (			
+		self::E_PARSE_ERROR 		=> "Parse error",
+		self::E_INVALID_REQUEST 	=> "Invalid Request",
+		self::E_METHOD_NOT_FOUND 	=> "Method not found",
+		self::E_INVALID_PARAMS 		=> "Invalid params",
+		self::E_INTERNAL_ERROR		=> "Internal error",
+		self::E_HTTP_NOT_FOUND		=> 'A resource matching URI "%1" was not found',
+		self::E_UNKNOWN_ERROR		=> "HTTP Unknown error"
+	);
+	
+	
+	// Errors inherited from Expresso Application.
+	static private $applicationErrors = array(			
+
+		self::LOGIN_SUCCESS_LOGOUT 			=> "You have been successfully logged out",
+		self::LOGIN_SESSION_EXPIRED 		=> "Sorry, your login has expired",
+		self::LOGIN_NOT_LOGGED_IN 			=> "You are not logged in",
+		self::LOGIN_COOKIES_REQUIRED 		=> "Cookies are required to login to this site.",
+		self::LOGIN_BADLOGIN 				=> "bad login or password",
+		self::LOGIN_PASSWORD_EXPIRED 		=> "Your password has expired, and you do not have access to change it",
+		self::LOGIN_AUTH_INVALID			=> "Your auth is invalid",
+		self::LOGIN_SESSION_NOT_VERIFIED	=> "Your session could not be verified.",
+		self::LOGIN_ACCOUNT_EXPIRED 		=> "Account is expired",
+		self::LOGIN_LOGIN_BLOCKED			=> "Blocked, too many attempts!",
+		self::LOGIN_INVALID_LOGIN			=> "Bad login or password",
+		self::CATALOG_MIN_ARGUMENT_SEARCH	=> "Your search argument must be longer than %1 characters."							
+	);
+		
+	static public function get($code, $arg){				
+		$errors = self::$reservedErrors + self::$applicationErrors;
+		$message = $errors[$code];
+		if($arg){
+			$message = str_replace("%1", $arg, $message);
+		}
+		if(!$message){
+			$message = $errors[self::E_UNKNOWN_ERROR];
+		}
+		
+		return array ("code" => $code, "message" => $message);				
+	}			
+		
+}
Index: /sandbox/webservice/api/rest/core/LoginResource.php
===================================================================
--- /sandbox/webservice/api/rest/core/LoginResource.php	(revision 6019)
+++ /sandbox/webservice/api/rest/core/LoginResource.php	(revision 6019)
@@ -0,0 +1,34 @@
+<?php
+
+class LoginResource extends Expresso {	
+	public function post($request){
+		// to Receive POST Params (use $this->params)
+ 		parent::post($request);	
+		
+		if(!$this-> isLoggedIn()) 
+		{					
+			if($sessionid = $GLOBALS['phpgw']->session->create($this->getParam('user'), $this->getParam('password')))					
+			{				
+				$result = array(
+					'auth' 			=> $sessionid.":".$GLOBALS['phpgw']->session->kp3,
+					'profile' 		=> array($this->getUserProfile())
+				);
+				$this->setError(false);
+				$this->setResult($result);
+			}
+			else
+			{	
+				$this->setError($GLOBALS['phpgw']->session->cd_reason);
+			}			
+		}
+		else
+		{
+			$result = array('auth' =>  $this->getParam('auth'));
+			$this->setResult($result);
+		}
+		
+		//to Send Response (JSON RPC format)
+		return $this->getResponse();		
+	}	
+
+}
Index: /sandbox/webservice/api/rest/core/LogoutResource.php
===================================================================
--- /sandbox/webservice/api/rest/core/LogoutResource.php	(revision 6019)
+++ /sandbox/webservice/api/rest/core/LogoutResource.php	(revision 6019)
@@ -0,0 +1,29 @@
+<?php
+class LogoutResource extends Expresso {
+
+	public function post($request){
+		// to Receive POST Params (use $this->params)
+		parent::post($request);
+		
+		if($this-> isLoggedIn())
+		{	if (file_exists($GLOBALS['phpgw_info']['server']['temp_dir'].SEP.$_SESSION['phpgw_session']['session_id']))	
+			{
+				$dh = opendir($GLOBALS['phpgw_info']['server']['temp_dir']. SEP . $_SESSION['phpgw_session']['session_id']);
+				while ($file = readdir($dh)) 
+				{
+					if ($file != '.' && $file != '..') 
+					{
+						unlink($GLOBALS['phpgw_info']['server']['temp_dir'].SEP.$_SESSION['phpgw_session']['session_id'].SEP.$file);
+					}
+				}
+				rmdir($GLOBALS['phpgw_info']['server']['temp_dir'].SEP.$_SESSION['phpgw_session']['session_id']);
+			}
+			$GLOBALS['phpgw']->hooks->process('logout');
+			$GLOBALS['phpgw']->session->destroy($_SESSION['phpgw_session']['session_id'], $GLOBALS['kp3']);
+			$this->setResult(true);
+		}
+		
+		//to Send Response (JSON RPC format)
+		return $this->getResponse();
+	}	
+}
Index: /sandbox/webservice/api/rest/core/ExpressoVersionResource.php
===================================================================
--- /sandbox/webservice/api/rest/core/ExpressoVersionResource.php	(revision 6019)
+++ /sandbox/webservice/api/rest/core/ExpressoVersionResource.php	(revision 6019)
@@ -0,0 +1,15 @@
+<?php
+
+class ExpressoVersionResource extends Expresso {		
+	public function post($request){
+		// to Receive POST Params (use $this->params)
+ 		parent::post($request); 		 
+ 		
+		$result = array('expressoVersion' =>  $this->expressoVersion);
+ 		$this->setResult($result);
+
+		//to Send Response (JSON RPC format)
+		return $this->getResponse(); 		
+	}
+
+}
Index: /sandbox/webservice/api/rest/core/Expresso.php
===================================================================
--- /sandbox/webservice/api/rest/core/Expresso.php	(revision 6019)
+++ /sandbox/webservice/api/rest/core/Expresso.php	(revision 6019)
@@ -0,0 +1,154 @@
+<?php
+class Expresso extends Resource {
+
+	protected $expressoVersion;
+	protected $ldapCatalog;
+	private $request;
+	private $params;
+	private $result;
+	private $error;	
+	private $id;
+	
+	function __construct($id){
+		$GLOBALS['phpgw_info'] = array(
+				'flags' => array(
+						'currentapp'        	=> 'login',
+						'noheader'              => True,
+						'disable_Template_class' => True
+				)
+		);
+		
+		include_once('../../header.inc.php');
+		$this->expressoVersion = substr($GLOBALS['phpgw_info']['server']['versions']['phpgwapi'],0,3);			
+	}
+	
+	protected function setRequest($request){
+		$this->request = $request;
+	}
+	
+	public function getRequest(){
+		return $this->request;
+	}
+	
+	protected function setResult($result){
+		$this->result = $result;
+	}
+	
+	public function getResult(){
+		return $this->result;
+	}
+	
+	protected function setId($id){
+		$this->id = $id;
+	}
+	
+	public function getId(){
+		return $this->id;
+	}
+	
+	protected function setParams($params){		
+		$this->params = $params;
+	}
+	
+	public function getParams(){	
+		return $this->params;
+	}
+	
+	public function getParam($param){
+		return $this->params->$param;
+	}
+	
+	public function setError($errorID, $arg1 = false) {
+		if($errorID)
+			$this-> error = Errors::get($errorID, $arg1);
+		else
+			$this-> error = null;
+	}
+	
+	protected function getError() {
+		return $this-> error;
+	}
+	
+	public function post($request){
+		$this->setRequest($request);		
+		parse_str($request->data, &$array);		
+		$data = (object)$array;		
+		if($data){
+			if($data->params){								
+				$this->setParams(json_decode($data->params));
+			}
+			if($data->id)
+				$this->setId($data->id);
+		}
+	}	
+	
+	public function get($request){
+		$response = new Response($request);
+		$response->code = Response::OK;
+		$response->addHeader('content-type', 'text/html');		
+		$response->body = "<H4>Metodo GET nao permitido para este recurso.</H4>";		
+		return $response;
+	}
+	
+	public function getResponse(){
+		$response = new Response($this->getRequest());
+		$response->code = Response::OK;
+		$response->addHeader('content-type', 'application/json');
+
+		
+		if($this->getResult()) {
+			$body['result']	= $this->getResult();
+		}
+		elseif($this->getError()){
+			$body['error'] = $this->getError();
+		}
+		else{
+			$this->setError(Errors::E_INTERNAL_ERROR);
+			$body['error'] = $this->getError();
+		}
+		
+		$response->body = json_encode($body);
+		
+		return $response;
+	}
+		
+	protected function getUserProfile(){
+		return array(
+			'contactID'			=> $GLOBALS['phpgw_info']['user']['account_id'],
+			'contactMails' 		=> array($GLOBALS['phpgw_info']['user']['email']),
+			'contactPhones' 	=> array($GLOBALS['phpgw_info']['user']['telephonenumber']),
+			'contactFullName'	=> $GLOBALS['phpgw_info']['user']['fullname']
+		);
+	}
+	protected function getLdapCatalog(){
+		if(!$this->ldapCatalog) {
+			$catalog_config = CreateObject("contactcenter.bo_ldap_manager");
+			$_SESSION['phpgw_info']['expressomail']['ldap_server'] = $catalog_config ? $catalog_config->srcs[1] : null;
+			$this->ldapCatalog = CreateObject("expressoMail1_2.ldap_functions");
+		}
+	
+		return $this->ldapCatalog;
+	}
+	
+	protected function getDb(){
+		return $GLOBALS['phpgw']->db;
+	}
+	
+	protected function isLoggedIn(){
+		if($this->getParam('auth') != null) {
+			list($sessionid, $kp3) = explode(":", $this->getParam('auth'));
+			if($GLOBALS['phpgw']->session->verify($sessionid, $kp3)){									
+				return true;
+			}
+			else{
+				$this-> setError(Errors::LOGIN_AUTH_INVALID);
+				return false;
+			}
+		}
+		else{
+			$this-> setError(Errors::LOGIN_NOT_LOGGED_IN);
+			return false;
+		}		
+	}	
+			
+}
Index: /sandbox/webservice/api/rest/catalog/Catalog.php
===================================================================
--- /sandbox/webservice/api/rest/catalog/Catalog.php	(revision 6019)
+++ /sandbox/webservice/api/rest/catalog/Catalog.php	(revision 6019)
@@ -0,0 +1,101 @@
+<?php
+
+
+class Catalog extends Expresso {	
+	var $minArgumentSearch;
+	var $userId;
+
+	public function __construct($id){
+		parent::__construct($id);
+		$prefs = $GLOBALS['phpgw']->preferences->read();
+		$this-> setMinArgumentSearch($prefs['expressoMail']['search_characters_number']);
+	}
+	
+	protected function setMinArgumentSearch($minArgumentSearch){
+		$this->minArgumentSearch = $minArgumentSearch;
+	}
+	
+	protected function getMinArgumentSearch(){
+		return $this-> minArgumentSearch;
+	}
+	
+	protected function getUserId(){
+		$userProfile = $this->getUserProfile();
+		return $userProfile['contactID'];
+	}		
+
+	protected function getUserLdapAttrs($mail)
+	{
+		$filter="(&(phpgwAccountType=u)(mail=".$mail."))";
+		$ldap_context = $_SESSION['phpgw_info']['expressomail']['ldap_server']['dn'];
+		$justthese = array("dn", 'jpegPhoto','givenName', 'sn'); 
+		$this->getLdapCatalog()->ldapConnect(true);
+		$ds = $this->getLdapCatalog()->ds;
+		if ($ds){
+			$sr = @ldap_search($ds, $ldap_context, $filter, $justthese);	
+			if ($sr) {
+				$entry = ldap_first_entry($ds, $sr);
+				if($entry) {									
+					$givenName = @ldap_get_values_len($ds, $entry, "givenname");
+					$sn = @ldap_get_values_len($ds, $entry, "sn");
+					$contactHasImagePicture = (@ldap_get_values_len($ds, $entry, "jpegphoto") ? 1 : 0);
+					$dn = ldap_get_dn($ds, $entry);
+					return array(
+						"contactID" => urlencode($dn),
+						"contactFirstName" => $givenName[0],
+						"contactLastName" 	=> $sn[0],
+						"contactHasImagePicture" => $contactHasImagePicture 
+					);
+				}
+			}
+		}
+		return false;
+	}
+	
+	protected function getUserLdapPhoto($contactID) {		
+		$ldap_context = $_SESSION['phpgw_info']['expressomail']['ldap_server']['dn'];
+		$justthese = array("dn", 'jpegPhoto','givenName', 'sn');
+		$this->getLdapCatalog()->ldapConnect(true);
+		$ds = $this->getLdapCatalog()->ds;		
+		
+		if ($ds){
+			$resource = @ldap_read($ds, $contactID, "phpgwaccounttype=u");
+			$n_entries = @ldap_count_entries($ds, $resource);
+
+			if ( $n_entries == 1) {			
+				$first_entry = ldap_first_entry($ds, $resource);
+				$obj = ldap_get_attributes($ds, $first_entry);
+				
+				if($obj['jpegPhoto']){
+					return ldap_get_values_len( $ds, $first_entry, "jpegPhoto");
+				}
+			}								
+		}
+		return false;
+	}	
+	
+	protected function getGlobalContacts($search, $uidNumber){
+		$contacts = array();
+		$params = array ("search_for" => $search);
+ 		$result = $this->getLdapCatalog()->quicksearch($params);
+
+		foreach($result as $i => $row) {
+			if(is_int($i)) {
+				$contacts[$i] = array(
+					'contactMails'	=> array($result[$i]['mail']),
+					'contactPhones'	=> array($result[$i]['phone']),
+					'contactAlias' => "",					
+					'contactFullName' 	=> ($result[$i]['cn'] != null ? mb_convert_encoding($row['cn'],"UTF8", "ISO_8859-1") : ""),
+					'contactBirthDate'	=> "",
+					'contactNotes' 		=> ""
+				);
+				// Buscar atributos faltantes. 
+				if(is_array($this->getUserLdapAttrs($result[$i]['mail'])))
+					$contacts[$i] = array_merge($this->getUserLdapAttrs($result[$i]['mail']), $contacts[$i]);				
+			}
+		}
+		$result = array ('contacts' => $contacts);
+		$this->setResult($result);
+		return $this->getResponse();
+	}
+}
Index: /sandbox/webservice/api/rest/catalog/ContactPictureResource.php
===================================================================
--- /sandbox/webservice/api/rest/catalog/ContactPictureResource.php	(revision 6019)
+++ /sandbox/webservice/api/rest/catalog/ContactPictureResource.php	(revision 6019)
@@ -0,0 +1,42 @@
+<?php
+
+class ContactPictureResource extends Catalog {	
+	public function post($request){
+		// to Receive POST Params (use $this->params)
+ 		parent::post($request);	
+		
+		if($this-> isLoggedIn() && $this->getParam('contactID') != null) 
+		{								
+			$contact = array();
+			// User Contact
+			if($this->getParam('contactType') == 1){
+				$query = 'select A.id_contact, A.photo from phpgw_cc_contact A where A.id_contact='.$this->getParam('contactID').' and A.id_owner='.$this -> getUserId();
+				if (!$this->getDb()->query($query))
+					return false;
+				if($this->getDb()->next_record()) {
+					$row = $this->getDb()->row();
+					if($row['photo'] != null) {
+						$contact[] = array(
+								'contactID'		=> $row['id_contact'],
+								'contactImagePicture'	=> ($row['photo'] != null ? base64_encode($row['photo']) : "")
+						);
+					}
+				}
+			}
+			// Global Catalog
+			elseif($this->getParam('contactType') == 2){
+				$photo = $this->getUserLdapPhoto(urldecode($this->getParam('contactID')));
+				$contact[] = array(
+						'contactID'		=> $this->getParam('contactID'),
+						'contactImagePicture'	=> ($photo != null ? base64_encode($photo[0]) : "")
+				);
+	
+			}
+			$result = array ('contacts' => $contact);
+			$this->setResult($result);			
+		}
+		//to Send Response (JSON RPC format)
+		return $this->getResponse();		
+	}	
+
+}
Index: /sandbox/webservice/api/rest/catalog/ContactsResource.php
===================================================================
--- /sandbox/webservice/api/rest/catalog/ContactsResource.php	(revision 6019)
+++ /sandbox/webservice/api/rest/catalog/ContactsResource.php	(revision 6019)
@@ -0,0 +1,79 @@
+<?php
+
+class ContactsResource extends Catalog {	
+	public function post($request){
+		// to Receive POST Params (use $this->params)
+ 		parent::post($request);	
+		
+		if($this-> isLoggedIn()) 
+		{			
+			$search = "";
+			
+			if($this->getParams()) {
+				$search = trim($this->getParam('search'));
+				
+				if($search != "") {
+					$search = mb_convert_encoding($search,"ISO_8859-1", "UTF8");
+				}
+		
+				if($this->getParam('contactID') > 0 && $this->getParam('contactType') == 1){
+					$query_contact = 'A.id_contact='.$this->getParam('contactID').' and';
+				}
+				elseif($this->getParam('contactType') == 2){
+					if($this-> getMinArgumentSearch() <= strlen($search))
+						return $this->getGlobalContacts($search, $this->getParam('contactID'));
+					else{
+						$this->setError(Errors::CATALOG_MIN_ARGUMENT_SEARCH, $this-> getMinArgumentSearch());						
+						return $this->getResponse();
+					}
+				}
+			}
+		
+			$query = 'select B.id_typeof_contact_connection, A.photo, A.id_contact, A.alias, A.given_names, A.family_names, A.names_ordered, A.birthdate, A.notes, C.connection_value from phpgw_cc_contact A, '.
+					'phpgw_cc_contact_conns B, phpgw_cc_connections C where A.id_contact = B.id_contact and B.id_connection = C.id_connection '.
+					' and '.$query_contact.' A.id_owner='.$this -> getUserId().' group by '.
+					' B.id_typeof_contact_connection, A.photo, A.id_contact, A.alias, A.given_names, A.family_names,A.names_ordered,A.birthdate, A.notes,C.connection_value	order by lower(A.names_ordered)';
+		
+			if (!$this->getDb()->query($query))
+				return false;
+		
+			$contacts = array();
+			while($this->getDb()->next_record()) {
+				$row = $this->getDb()->row();
+				$id = $row['id_contact'];
+				$contactType = ($row['id_typeof_contact_connection'] == 2 ? 'contactPhones' : 'contactMails');
+		
+				if($contacts[$id] != null){
+					$contacts[$id][$contactType][] = $row['connection_value'];
+				}
+				else{
+					$contacts[$id] = array(
+							'contactID'		=> $row['id_contact'],
+							$contactType	=> array($row['connection_value']),
+							'contactAlias' => ($row['alias'] != null ?  mb_convert_encoding($row['alias'],"UTF8", "ISO_8859-1") : ""),
+							'contactFirstName'	=> ($row['given_names'] != null ?  mb_convert_encoding($row['given_names'],"UTF8", "ISO_8859-1") : ""),
+							'contactLastName' 	=> ($row['family_names'] != null ?  mb_convert_encoding($row['family_names'],"UTF8", "ISO_8859-1") : ""),
+							'contactFullName' 	=> ($row['names_ordered'] != null ? mb_convert_encoding($row['names_ordered'],"UTF8", "ISO_8859-1") : ""),
+							'contactBirthDate'	=> ($row['birthdate'] != null ? $row['birthdate'] : ""),
+							'contactNotes' 		=> ($row['notes'] != null ?  mb_convert_encoding($row['notes'],"UTF8", "ISO_8859-1") : ""),
+							'contactHasImagePicture' => ($row['photo'] != null ? 1 : 0),
+					);
+					if($search != null &&
+							stristr($contacts[$id]['contactAlias'], $search) == null &&
+							stristr($contacts[$id]['contactFirstName'], $search) == null &&
+							stristr($contacts[$id]['contactLastName'], $search) == null &&
+							stristr($contacts[$id]['contactFullName'], $search) == null &&
+							stristr($contacts[$id]['contactBirthDate'], $search) == null &&
+							stristr($contacts[$id]['contactNotes'], $search) == null){
+						unset($contacts[$id]);
+					}
+				}
+			}
+			$result = array ('contacts' => array_values($contacts));
+			$this->setResult($result);
+		}
+		//to Send Response (JSON RPC format)
+		return $this->getResponse();		
+	}	
+
+}
Index: /sandbox/webservice/api/rest/.htaccess
===================================================================
--- /sandbox/webservice/api/rest/.htaccess	(revision 6019)
+++ /sandbox/webservice/api/rest/.htaccess	(revision 6019)
@@ -0,0 +1,6 @@
+<IfModule mod_rewrite.c>
+    RewriteEngine On
+    RewriteCond %{REQUEST_URI} !dispatch\.php$
+    RewriteCond %{REQUEST_FILENAME} !-f
+    RewriteRule .* dispatch.php [L,QSA]
+</IfModule>
