Browse Source

Extracting asset dispatcher and cache dispatcher into separate classes to provide examples on how to use Dispatcher
Filters

Jose Lorenzo Rodriguez 14 years ago
parent
commit
826699a670

+ 22 - 0
app/Config/bootstrap.php

@@ -122,3 +122,25 @@ Cache::config('default', array('engine' => 'File'));
  * CakePlugin::load('DebugKit'); //Loads a single plugin named DebugKit
  *
  */
+
+
+/**
+ * You can attach event listeners to the request lifecyle as Dispatcher Filter . By Default CakePHP bundles two filters:
+ *
+ * - AssetDispatcher filter will serve your asset files (css, images, js, etc) from your themes and plugins
+ * - CacheDispatcher filter will read the Cache.check configure variable and try to serve cached content generated from controllers
+ *
+ * Feel free to remove or add filters as you see fit for your application. A few examples:
+ *
+ * Configure::write('Dispatcher.filters', array(
+ *		'MyCacheFilter', //  will use MyCacheFilter class from the Routing/Filter package in your app.
+ *		'MyPlugin.MyFilter', // will use MyFilter class from the Routing/Filter package in MyPlugin plugin.
+ * 		array('callbale' => $aFunction, 'on' => 'before', 'priority' => 9), // A valid PHP callback type to be called on beforeDispatch
+ *		array('callbale' => $anotherMethod, 'on' => 'after'), // A valid PHP callback type to be called on afterDispatch
+ *
+ * ));
+ */
+Configure::write('Dispatcher.filters', array(
+	'AssetDispatcher',
+	'CacheDispatcher'
+));

+ 21 - 0
lib/Cake/Console/Templates/skel/Config/bootstrap.php

@@ -63,3 +63,24 @@ Cache::config('default', array('engine' => 'File'));
  * CakePlugin::load('DebugKit'); //Loads a single plugin named DebugKit
  *
  */
+
+/**
+ * You can attach event listeners to the request lifecyle as Dispatcher Filter . By Default CakePHP bundles two filters:
+ *
+ * - AssetDispatcher filter will serve your asset files (css, images, js, etc) from your themes and plugins
+ * - CacheDispatcher filter will read the Cache.check configure variable and try to serve cached content generated from controllers
+ *
+ * Feel free to remove or add filters as you see fit for your application. A few examples:
+ *
+ * Configure::write('Dispatcher.filters', array(
+ *		'MyCacheFilter', //  will use MyCacheFilter class from the Routing/Filter package in your app.
+ *		'MyPlugin.MyFilter', // will use MyFilter class from the Routing/Filter package in MyPlugin plugin.
+ * 		array('callbale' => $aFunction, 'on' => 'before', 'priority' => 9), // A valid PHP callback type to be called on beforeDispatch
+ *		array('callbale' => $anotherMethod, 'on' => 'after'), // A valid PHP callback type to be called on afterDispatch
+ *
+ * ));
+ */
+Configure::write('Dispatcher.filters', array(
+	'AssetDispatcher',
+	'CacheDispatcher'
+));

+ 1 - 144
lib/Cake/Routing/Dispatcher.php

@@ -79,13 +79,7 @@ class Dispatcher implements CakeEventListener {
  * @return array
  **/
 	public function implementedEvents() {
-		return array(
-			'Dispatcher.beforeDispatch' => array(
-				array('callable' => array($this, 'asset')),
-				array('callable' => array($this, 'cached')),
-				array('callable' => array($this, 'parseParams')),
-			)
-		);
+		return array('Dispatcher.beforeDispatch' => 'parseParams');
 	}
 
 /**
@@ -283,141 +277,4 @@ class Dispatcher implements CakeEventListener {
 		include APP . 'Config' . DS . 'routes.php';
 	}
 
-/**
- * Checks whether the response was cached and set the body accordingly.
- *
- * @param CakeEvent $event containing the request and response object
- * @return CakeResponse with cached content if found, null otherwise
- */
-	public function cached($event) {
-		$path = $event->data['request']->here();
-		if (Configure::read('Cache.check') === true) {
-			if ($path == '/') {
-				$path = 'home';
-			}
-			$path = strtolower(Inflector::slug($path));
-
-			$filename = CACHE . 'views' . DS . $path . '.php';
-
-			if (!file_exists($filename)) {
-				$filename = CACHE . 'views' . DS . $path . '_index.php';
-			}
-			if (file_exists($filename)) {
-				$controller = null;
-				$view = new View($controller);
-				$result = $view->renderCache($filename, microtime(true));
-				if ($result !== false) {
-					$event->data['response']->body($result);
-					return $event->data['response'];
-				}
-			}
-		}
-	}
-
-/**
- * Checks if a requested asset exists and sends it to the browser
- *
- * @param CakeEvent $event containing the request and response object
- * @return CakeResponse if the client is requesting a recognized asset, null otherwise
- */
-	public function asset($event) {
-		$url = $event->data['request']->url;
-		$response = $event->data['response'];
-
-		if (strpos($url, '..') !== false || strpos($url, '.') === false) {
-			return;
-		}
-
-		$filters = Configure::read('Asset.filter');
-		$isCss = (
-			strpos($url, 'ccss/') === 0 ||
-			preg_match('#^(theme/([^/]+)/ccss/)|(([^/]+)(?<!css)/ccss)/#i', $url)
-		);
-		$isJs = (
-			strpos($url, 'cjs/') === 0 ||
-			preg_match('#^/((theme/[^/]+)/cjs/)|(([^/]+)(?<!js)/cjs)/#i', $url)
-		);
-
-		if (($isCss && empty($filters['css'])) || ($isJs && empty($filters['js']))) {
-			$response->statusCode(404);
-			$event->stopPropagation();
-			return $response;
-		} elseif ($isCss) {
-			include WWW_ROOT . DS . $filters['css'];
-			$event->stopPropagation();
-			return $response;
-		} elseif ($isJs) {
-			include WWW_ROOT . DS . $filters['js'];
-			$event->stopPropagation();
-			return $response;
-		}
-
-		$pathSegments = explode('.', $url);
-		$ext = array_pop($pathSegments);
-		$parts = explode('/', $url);
-		$assetFile = null;
-
-		if ($parts[0] === 'theme') {
-			$themeName = $parts[1];
-			unset($parts[0], $parts[1]);
-			$fileFragment = urldecode(implode(DS, $parts));
-			$path = App::themePath($themeName) . 'webroot' . DS;
-			if (file_exists($path . $fileFragment)) {
-				$assetFile = $path . $fileFragment;
-			}
-		} else {
-			$plugin = Inflector::camelize($parts[0]);
-			if (CakePlugin::loaded($plugin)) {
-				unset($parts[0]);
-				$fileFragment = urldecode(implode(DS, $parts));
-				$pluginWebroot = CakePlugin::path($plugin) . 'webroot' . DS;
-				if (file_exists($pluginWebroot . $fileFragment)) {
-					$assetFile = $pluginWebroot . $fileFragment;
-				}
-			}
-		}
-
-		if ($assetFile !== null) {
-			$event->stopPropagation();
-			$this->_deliverAsset($response, $assetFile, $ext);
-			return $response;
-		}
-	}
-
-/**
- * Sends an asset file to the client
- *
- * @param CakeResponse $response The response object to use.
- * @param string $assetFile Path to the asset file in the file system
- * @param string $ext The extension of the file to determine its mime type
- * @return void
- */
-	protected function _deliverAsset(CakeResponse $response, $assetFile, $ext) {
-		ob_start();
-		$compressionEnabled = Configure::read('Asset.compress') && $response->compress();
-		if ($response->type($ext) == $ext) {
-			$contentType = 'application/octet-stream';
-			$agent = env('HTTP_USER_AGENT');
-			if (preg_match('%Opera(/| )([0-9].[0-9]{1,2})%', $agent) || preg_match('/MSIE ([0-9].[0-9]{1,2})/', $agent)) {
-				$contentType = 'application/octetstream';
-			}
-			$response->type($contentType);
-		}
-		if (!$compressionEnabled) {
-			$response->header('Content-Length', filesize($assetFile));
-		}
-		$response->cache(filemtime($assetFile));
-		$response->send();
-		ob_clean();
-		if ($ext === 'css' || $ext === 'js') {
-			include $assetFile;
-		} else {
-			readfile($assetFile);
-		}
-
-		if ($compressionEnabled) {
-			ob_end_flush();
-		}
-	}
-
 }

+ 1 - 1
lib/Cake/Routing/DispatcherFilter.php

@@ -23,7 +23,7 @@ App::uses('CakeEventListener', 'Event');
  * event listener with the ability to alter the request or response as needed before it is handled
  * by a controller or after the response body has already been built.
  *
- * @package Cake.Event
+ * @package Cake.Routing
  */
 abstract class DispatcherFilter implements CakeEventListener {
 

+ 156 - 0
lib/Cake/Routing/Filter/AssetDispatcher.php

@@ -0,0 +1,156 @@
+<?php
+/**
+ *
+ * PHP 5
+ *
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright	  Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link		  http://cakephp.org CakePHP(tm) Project
+ * @package		  Cake.Routing
+ * @since		  CakePHP(tm) v 2.2
+ * @license		  MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+App::uses('DispatcherFilter', 'Routing');
+
+/**
+ * Filters a request and tests whether it is a file in the webroot folder or not and
+ * serves the file to the client if appropriate.
+ *
+ * @package Cake.Routing.Filter
+ */
+class AssetDispatcher extends DispatcherFilter {
+
+/**
+ * Default priority for all methods in this filter
+ * This filter should run before the request gets parsed by router
+ *
+ * @var int
+ **/
+	public $priority = 9;
+
+/**
+ * Checks if a requested asset exists and sends it to the browser
+ *
+ * @param CakeEvent $event containing the request and response object
+ * @return CakeResponse if the client is requesting a recognized asset, null otherwise
+ */
+	public function beforeDispatch($event) {
+		$url = $event->data['request']->url;
+		$response = $event->data['response'];
+
+		if (strpos($url, '..') !== false || strpos($url, '.') === false) {
+			return;
+		}
+
+		if ($result = $this->_filterAsset($event)) {
+			$event->stopPropagation();
+			return $result;
+		}
+
+		$pathSegments = explode('.', $url);
+		$ext = array_pop($pathSegments);
+		$parts = explode('/', $url);
+		$assetFile = null;
+
+		if ($parts[0] === 'theme') {
+			$themeName = $parts[1];
+			unset($parts[0], $parts[1]);
+			$fileFragment = urldecode(implode(DS, $parts));
+			$path = App::themePath($themeName) . 'webroot' . DS;
+			if (file_exists($path . $fileFragment)) {
+				$assetFile = $path . $fileFragment;
+			}
+		} else {
+			$plugin = Inflector::camelize($parts[0]);
+			if (CakePlugin::loaded($plugin)) {
+				unset($parts[0]);
+				$fileFragment = urldecode(implode(DS, $parts));
+				$pluginWebroot = CakePlugin::path($plugin) . 'webroot' . DS;
+				if (file_exists($pluginWebroot . $fileFragment)) {
+					$assetFile = $pluginWebroot . $fileFragment;
+				}
+			}
+		}
+
+		if ($assetFile !== null) {
+			$event->stopPropagation();
+			$this->_deliverAsset($response, $assetFile, $ext);
+			return $response;
+		}
+	}
+
+/**
+ * Checks if the client is requeting a filtered asset and runs the corresponding
+ * filter if any is configured
+ *
+ * @param CakeEvent $event containing the request and response object
+ * @return CakeResponse if the client is requesting a recognized asset, null otherwise
+ */
+	protected function _filterAsset($event) {
+		$url = $event->data['request']->url;
+		$response = $event->data['response'];
+		$filters = Configure::read('Asset.filter');
+		$isCss = (
+			strpos($url, 'ccss/') === 0 ||
+			preg_match('#^(theme/([^/]+)/ccss/)|(([^/]+)(?<!css)/ccss)/#i', $url)
+		);
+		$isJs = (
+			strpos($url, 'cjs/') === 0 ||
+			preg_match('#^/((theme/[^/]+)/cjs/)|(([^/]+)(?<!js)/cjs)/#i', $url)
+		);
+
+		if (($isCss && empty($filters['css'])) || ($isJs && empty($filters['js']))) {
+			$response->statusCode(404);
+			return $response;
+		} elseif ($isCss) {
+			include WWW_ROOT . DS . $filters['css'];
+			return $response;
+		} elseif ($isJs) {
+			include WWW_ROOT . DS . $filters['js'];
+			return $response;
+		}
+	}
+
+/**
+ * Sends an asset file to the client
+ *
+ * @param CakeResponse $response The response object to use.
+ * @param string $assetFile Path to the asset file in the file system
+ * @param string $ext The extension of the file to determine its mime type
+ * @return void
+ */
+	protected function _deliverAsset(CakeResponse $response, $assetFile, $ext) {
+		ob_start();
+		$compressionEnabled = Configure::read('Asset.compress') && $response->compress();
+		if ($response->type($ext) == $ext) {
+			$contentType = 'application/octet-stream';
+			$agent = env('HTTP_USER_AGENT');
+			if (preg_match('%Opera(/| )([0-9].[0-9]{1,2})%', $agent) || preg_match('/MSIE ([0-9].[0-9]{1,2})/', $agent)) {
+				$contentType = 'application/octetstream';
+			}
+			$response->type($contentType);
+		}
+		if (!$compressionEnabled) {
+			$response->header('Content-Length', filesize($assetFile));
+		}
+		$response->cache(filemtime($assetFile));
+		$response->send();
+		ob_clean();
+		if ($ext === 'css' || $ext === 'js') {
+			include $assetFile;
+		} else {
+			readfile($assetFile);
+		}
+
+		if ($compressionEnabled) {
+			ob_end_flush();
+		}
+	}
+
+}

+ 70 - 0
lib/Cake/Routing/Filter/CacheDispatcher.php

@@ -0,0 +1,70 @@
+<?php
+/**
+ *
+ * PHP 5
+ *
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright	  Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link		  http://cakephp.org CakePHP(tm) Project
+ * @package		  Cake.Routing
+ * @since		  CakePHP(tm) v 2.2
+ * @license		  MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+App::uses('DispatcherFilter', 'Routing');
+
+/**
+ * This filter will check wheter the response was previously cached in the file system
+ * and served it back to the client if appropriate.
+ *
+ * @package Cake.Routing.Filter
+ */
+class CacheDispatcher extends DispatcherFilter {
+
+/**
+ * Default priority for all methods in this filter
+ * This filter should run before the request gets parsed by router
+ *
+ * @var int
+ **/
+	public $priority = 9;
+
+/**
+ * Checks whether the response was cached and set the body accordingly.
+ *
+ * @param CakeEvent $event containing the request and response object
+ * @return CakeResponse with cached content if found, null otherwise
+ */
+	public function beforeDispatch($event) {
+		if (Configure::read('Cache.check') !== true) {
+			return;
+		}
+
+		$path = $event->data['request']->here();		
+		if ($path == '/') {
+			$path = 'home';
+		}
+		$path = strtolower(Inflector::slug($path));
+
+		$filename = CACHE . 'views' . DS . $path . '.php';
+
+		if (!file_exists($filename)) {
+			$filename = CACHE . 'views' . DS . $path . '_index.php';
+		}
+		if (file_exists($filename)) {
+			$controller = null;
+			$view = new View($controller);
+			$result = $view->renderCache($filename, microtime(true));
+			if ($result !== false) {
+				$event->data['response']->body($result);
+				return $event->data['response'];
+			}
+		}
+	}
+
+}

+ 1 - 0
lib/Cake/Test/Case/AllRoutingTest.php

@@ -38,6 +38,7 @@ class AllRoutingTest extends PHPUnit_Framework_TestSuite {
 
 		$suite->addTestDirectory($libs . 'Routing');
 		$suite->addTestDirectory($libs . 'Routing' . DS . 'Route');
+		$suite->addTestDirectory($libs . 'Routing' . DS . 'Filter');
 		return $suite;
 	}
 }

+ 10 - 49
lib/Cake/Test/Case/Routing/DispatcherTest.php

@@ -1357,6 +1357,7 @@ class DispatcherTest extends CakeTestCase {
 			'View' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'View' . DS)
 		));
 		CakePlugin::load(array('TestPlugin', 'TestPluginTwo'));
+		Configure::write('Dispatcher.filters', array('AssetDispatcher'));
 
 		$Dispatcher = new TestDispatcher();
 		$response = $this->getMock('CakeResponse', array('_sendHeader'));
@@ -1377,7 +1378,7 @@ class DispatcherTest extends CakeTestCase {
 	}
 
 /**
- * Data provider for asset()
+ * Data provider for asset filter
  *
  * - theme assets.
  * - plugin assets.
@@ -1475,6 +1476,7 @@ class DispatcherTest extends CakeTestCase {
 			'View' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'View' . DS)
 		));
 		CakePlugin::load(array('TestPlugin', 'PluginJs'));
+		Configure::write('Dispatcher.filters', array('AssetDispatcher'));
 
 		$Dispatcher = new TestDispatcher();
 		$response = $this->getMock('CakeResponse', array('_sendHeader'));
@@ -1503,57 +1505,13 @@ class DispatcherTest extends CakeTestCase {
 			'js' => '',
 			'css' => null
 		));
+		Configure::write('Dispatcher.filters', array('AssetDispatcher'));
 
 		$request = new CakeRequest('ccss/cake.generic.css');
-		$event = new CakeEvent('DispatcherTest', $Dispatcher, compact('request', 'response'));
-		$this->assertSame($response, $Dispatcher->asset($event));
+		$Dispatcher->dispatch($request, $response);
 		$this->assertEquals('404', $response->statusCode());
-		$this->assertTrue($event->isStopped());
 	}
 
-/**
- * test that asset filters work for theme and plugin assets
- *
- * @return void
- */
-	public function testAssetFilterForThemeAndPlugins() {
-		$Dispatcher = new TestDispatcher();
-		$response = $this->getMock('CakeResponse', array('_sendHeader'));
-		Configure::write('Asset.filter', array(
-			'js' => '',
-			'css' => ''
-		));
-
-		$request = new CakeRequest('theme/test_theme/ccss/cake.generic.css');
-		$event = new CakeEvent('DispatcherTest', $Dispatcher, compact('request', 'response'));
-		$this->assertSame($response, $Dispatcher->asset($event));
-		$this->assertTrue($event->isStopped());
-
-		$request = new CakeRequest('theme/test_theme/cjs/debug_kit.js');
-		$event = new CakeEvent('DispatcherTest', $Dispatcher, compact('request', 'response'));
-		$this->assertSame($response, $Dispatcher->asset($event));
-		$this->assertTrue($event->isStopped());
-
-		$request = new CakeRequest('test_plugin/ccss/cake.generic.css');
-		$event = new CakeEvent('DispatcherTest', $Dispatcher, compact('request', 'response'));
-		$this->assertSame($response, $Dispatcher->asset($event));
-		$this->assertTrue($event->isStopped());
-
-		$request = new CakeRequest('test_plugin/cjs/debug_kit.js');
-		$event = new CakeEvent('DispatcherTest', $Dispatcher, compact('request', 'response'));
-		$this->assertSame($response, $Dispatcher->asset($event));
-		$this->assertTrue($event->isStopped());
-
-		$request = new CakeRequest('css/ccss/debug_kit.css');
-		$event = new CakeEvent('DispatcherTest', $Dispatcher, compact('request', 'response'));
-		$this->assertNull($Dispatcher->asset($event));
-		$this->assertFalse($event->isStopped());
-
-		$request = new CakeRequest('js/cjs/debug_kit.js');
-		$event = new CakeEvent('DispatcherTest', $Dispatcher, compact('request', 'response'));
-		$this->assertNull($Dispatcher->asset($event));
-		$this->assertFalse($event->isStopped());
-	}
 
 /**
  * Data provider for cached actions.
@@ -1606,8 +1564,11 @@ class DispatcherTest extends CakeTestCase {
 		$dispatcher->dispatch($request, $response);
 		$out = $response->body();
 
-		$event = new CakeEvent('DispatcherTest', $dispatcher, array('request' => $request, 'response' => $response));
-		$response = $dispatcher->cached($event);
+		Configure::write('Dispatcher.filters', array('CacheDispatcher'));
+		$request = new CakeRequest($url);
+		$response = $this->getMock('CakeResponse', array('send'));
+		$dispatcher = new TestDispatcher();
+		$dispatcher->dispatch($request, $response);
 		$cached = $response->body();
 
 		$cached = preg_replace('/<!--+[^<>]+-->/', '', $cached);

+ 78 - 0
lib/Cake/Test/Case/Routing/Filter/AssetDispatcherTest.php

@@ -0,0 +1,78 @@
+<?php
+/**
+ * RouterTest file
+ *
+ * PHP 5
+ *
+ * CakePHP(tm) Tests <http://book.cakephp.org/view/1196/Testing>
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ *	Licensed under The Open Group Test Suite License
+ *	Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://book.cakephp.org/view/1196/Testing CakePHP(tm) Tests
+ * @package       Cake.Test.Case.Routing.Filter
+ * @since         CakePHP(tm) v 2.2
+ * @license       MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+App::uses('AssetDispatcher', 'Routing/Filter');
+App::uses('CakeEvent', 'Event');
+App::uses('CakeResponse', 'Network');
+
+class AssetDispatcherTest extends CakeTestCase {
+
+/**
+ * tearDown method
+ *
+ * @return void
+ */
+	public function tearDown() {
+		Configure::write('Dispatcher.filters', array());
+	}
+
+/**
+ * test that asset filters work for theme and plugin assets
+ *
+ * @return void
+ */
+	public function testAssetFilterForThemeAndPlugins() {
+		$filter = new AssetDispatcher();
+		$response = $this->getMock('CakeResponse', array('_sendHeader'));
+		Configure::write('Asset.filter', array(
+			'js' => '',		
+			'css' => ''
+		));
+
+		$request = new CakeRequest('theme/test_theme/ccss/cake.generic.css');
+		$event = new CakeEvent('DispatcherTest', $this, compact('request', 'response'));
+		$this->assertSame($response, $filter->beforeDispatch($event));
+		$this->assertTrue($event->isStopped());
+
+		$request = new CakeRequest('theme/test_theme/cjs/debug_kit.js');
+		$event = new CakeEvent('DispatcherTest', $this, compact('request', 'response'));
+		$this->assertSame($response, $filter->beforeDispatch($event));
+		$this->assertTrue($event->isStopped());
+
+		$request = new CakeRequest('test_plugin/ccss/cake.generic.css');
+		$event = new CakeEvent('DispatcherTest', $this, compact('request', 'response'));
+		$this->assertSame($response, $filter->beforeDispatch($event));
+		$this->assertTrue($event->isStopped());
+
+		$request = new CakeRequest('test_plugin/cjs/debug_kit.js');
+		$event = new CakeEvent('DispatcherTest', $this, compact('request', 'response'));
+		$this->assertSame($response, $filter->beforeDispatch($event));
+		$this->assertTrue($event->isStopped());
+
+		$request = new CakeRequest('css/ccss/debug_kit.css');
+		$event = new CakeEvent('DispatcherTest', $this, compact('request', 'response'));
+		$this->assertNull($filter->beforeDispatch($event));
+		$this->assertFalse($event->isStopped());
+
+		$request = new CakeRequest('js/cjs/debug_kit.js');
+		$event = new CakeEvent('DispatcherTest', $this, compact('request', 'response'));
+		$this->assertNull($filter->beforeDispatch($event));
+		$this->assertFalse($event->isStopped());
+	}
+}