[FEATURE] Fallback paths (backport from Flow) 50/23950/10
authorTymoteusz Motylewski <t.motylewski@gmail.com>
Sat, 21 Sep 2013 13:15:53 +0000 (15:15 +0200)
committerAnja Leichsenring <aleichsenring@ab-softlab.de>
Sat, 12 Oct 2013 09:58:04 +0000 (11:58 +0200)
With this change it will be possible to define multiple values
for the 'template', 'partial' and 'layout' root path configuration.
Each of the respective options will now have a corresponding setter
that enables configuration of multiple paths to look up when loading a
Fluid template file:
$view->setTemplateRootPaths(array('first/path', 'second/path', …));
The old setters will be kept and they overrule the fallback paths. So:
$view->setTemplateRootPath('some/path');
would disable the fallback paths of the previous example. The same is true
for 'setPartialRootPath()' and 'setLayoutRootPath()'.
The rootPath-getters have been deprecated in favor of
'getTemplateRootPaths()', 'getPartialRootPaths()' and
'getLayoutRootPaths()'.

This is a backport of the Flow feature applied in
Change-Id: I530e9a1fadbbd210c980c62cf2022c38fa81bb56 issue #39870

Resolves: #39868
Releases: 6.2
Change-Id: Id5a768ae834c53cd20fd59e762c2acf2ea9e6356
Reviewed-on: https://review.typo3.org/23950
Reviewed-by: Frans Saris
Tested-by: Frans Saris
Reviewed-by: Philipp Gampe
Tested-by: Philipp Gampe
Reviewed-by: Anja Leichsenring
Tested-by: Anja Leichsenring
typo3/sysext/extbase/Classes/Mvc/Controller/ActionController.php
typo3/sysext/fluid/Classes/View/TemplateView.php
typo3/sysext/fluid/Tests/Unit/View/TemplateViewTest.php

index 07d10ed..7edef20 100644 (file)
@@ -27,6 +27,9 @@ namespace TYPO3\CMS\Extbase\Mvc\Controller;
  *
  *  This copyright notice MUST APPEAR in all copies of the script!
  ***************************************************************/
+use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
+use TYPO3\CMS\Extbase\Mvc\View\ViewInterface;
+
 /**
  * A multi action controller. This is by far the most common base class for Controllers.
  *
@@ -49,7 +52,7 @@ class ActionController extends \TYPO3\CMS\Extbase\Mvc\Controller\AbstractControl
        /**
         * The current view, as resolved by resolveView()
         *
-        * @var \TYPO3\CMS\Extbase\Mvc\View\ViewInterface
+        * @var ViewInterface
         * @api
         */
        protected $view = NULL;
@@ -331,7 +334,7 @@ class ActionController extends \TYPO3\CMS\Extbase\Mvc\Controller\AbstractControl
                                $actionResult = call_user_func_array(array($this, $this->actionMethodName), $preparedArguments);
                        }
                }
-               if ($actionResult === NULL && $this->view instanceof \TYPO3\CMS\Extbase\Mvc\View\ViewInterface) {
+               if ($actionResult === NULL && $this->view instanceof ViewInterface) {
                        $this->response->appendContent($this->view->render());
                } elseif (is_string($actionResult) && strlen($actionResult) > 0) {
                        $this->response->appendContent($actionResult);
@@ -351,7 +354,7 @@ class ActionController extends \TYPO3\CMS\Extbase\Mvc\Controller\AbstractControl
        protected function resolveView() {
                $viewObjectName = $this->resolveViewObjectName();
                if ($viewObjectName !== FALSE) {
-                       /** @var $view \TYPO3\CMS\Extbase\Mvc\View\ViewInterface */
+                       /** @var $view ViewInterface */
                        $view = $this->objectManager->get($viewObjectName);
                        $this->setViewConfiguration($view);
                        if ($view->canRender($this->controllerContext) === FALSE) {
@@ -359,7 +362,7 @@ class ActionController extends \TYPO3\CMS\Extbase\Mvc\Controller\AbstractControl
                        }
                }
                if (!isset($view) && $this->defaultViewObjectName != '') {
-                       /** @var $view \TYPO3\CMS\Extbase\Mvc\View\ViewInterface */
+                       /** @var $view ViewInterface */
                        $view = $this->objectManager->get($this->defaultViewObjectName);
                        $this->setViewConfiguration($view);
                        if ($view->canRender($this->controllerContext) === FALSE) {
@@ -383,20 +386,51 @@ class ActionController extends \TYPO3\CMS\Extbase\Mvc\Controller\AbstractControl
        }
 
        /**
-        * @param \TYPO3\CMS\Extbase\Mvc\View\ViewInterface $view
+        * @param ViewInterface $view
+        *
         * @return void
         */
-       protected function setViewConfiguration(\TYPO3\CMS\Extbase\Mvc\View\ViewInterface $view) {
+       protected function setViewConfiguration(ViewInterface $view) {
                // Template Path Override
-               $extbaseFrameworkConfiguration = $this->configurationManager->getConfiguration(\TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
-               if (isset($extbaseFrameworkConfiguration['view']['templateRootPath']) && strlen($extbaseFrameworkConfiguration['view']['templateRootPath']) > 0 && method_exists($view, 'setTemplateRootPath')) {
-                       $view->setTemplateRootPath(\TYPO3\CMS\Core\Utility\GeneralUtility::getFileAbsFileName($extbaseFrameworkConfiguration['view']['templateRootPath']));
+               $extbaseFrameworkConfiguration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
+               if (isset($extbaseFrameworkConfiguration['view']['templateRootPath'])
+                       && strlen($extbaseFrameworkConfiguration['view']['templateRootPath']) > 0
+                       && method_exists($view, 'setTemplateRootPath')
+               ) {
+                       $view->setTemplateRootPath($extbaseFrameworkConfiguration['view']['templateRootPath']);
+               } elseif (!empty($extbaseFrameworkConfiguration['view']['templateRootPaths'])
+                       && is_array($extbaseFrameworkConfiguration['view']['templateRootPaths'])
+                       && method_exists($view, 'setTemplateRootPaths')
+               ) {
+                       $paths = $extbaseFrameworkConfiguration['view']['templateRootPaths'];
+                       krsort($paths);
+                       $view->setTemplateRootPaths($paths);
                }
-               if (isset($extbaseFrameworkConfiguration['view']['layoutRootPath']) && strlen($extbaseFrameworkConfiguration['view']['layoutRootPath']) > 0 && method_exists($view, 'setLayoutRootPath')) {
-                       $view->setLayoutRootPath(\TYPO3\CMS\Core\Utility\GeneralUtility::getFileAbsFileName($extbaseFrameworkConfiguration['view']['layoutRootPath']));
+               if (isset($extbaseFrameworkConfiguration['view']['layoutRootPath'])
+                       && strlen($extbaseFrameworkConfiguration['view']['layoutRootPath']) > 0
+                       && method_exists($view, 'setLayoutRootPath')
+               ) {
+                       $view->setLayoutRootPath($extbaseFrameworkConfiguration['view']['layoutRootPath']);
+               } elseif (!empty($extbaseFrameworkConfiguration['view']['layoutRootPaths'])
+                       && is_array($extbaseFrameworkConfiguration['view']['layoutRootPaths'])
+                       && method_exists($view, 'layoutRootPaths')
+               ) {
+                       $paths = $extbaseFrameworkConfiguration['view']['layoutRootPaths'];
+                       krsort($paths);
+                       $view->setLayoutRootPaths($paths);
                }
-               if (isset($extbaseFrameworkConfiguration['view']['partialRootPath']) && strlen($extbaseFrameworkConfiguration['view']['partialRootPath']) > 0 && method_exists($view, 'setPartialRootPath')) {
-                       $view->setPartialRootPath(\TYPO3\CMS\Core\Utility\GeneralUtility::getFileAbsFileName($extbaseFrameworkConfiguration['view']['partialRootPath']));
+               if (isset($extbaseFrameworkConfiguration['view']['partialRootPath'])
+                       && strlen($extbaseFrameworkConfiguration['view']['partialRootPath']) > 0
+                       && method_exists($view, 'setPartialRootPath')
+               ) {
+                       $view->setPartialRootPath($extbaseFrameworkConfiguration['view']['partialRootPath']);
+               } elseif (!empty($extbaseFrameworkConfiguration['view']['partialRootPaths'])
+                       && is_array($extbaseFrameworkConfiguration['view']['partialRootPaths'])
+                       && method_exists($view, 'setPartialRootPaths')
+               ) {
+                       $paths = $extbaseFrameworkConfiguration['view']['partialRootPaths'];
+                       krsort($paths);
+                       $view->setPartialRootPaths($paths);
                }
        }
 
@@ -436,11 +470,11 @@ class ActionController extends \TYPO3\CMS\Extbase\Mvc\Controller\AbstractControl
         * Override this method to solve assign variables common for all actions
         * or prepare the view in another way before the action is called.
         *
-        * @param \TYPO3\CMS\Extbase\Mvc\View\ViewInterface $view The view to be initialized
+        * @param ViewInterface $view The view to be initialized
         * @return void
         * @api
         */
-       protected function initializeView(\TYPO3\CMS\Extbase\Mvc\View\ViewInterface $view) {
+       protected function initializeView(ViewInterface $view) {
        }
 
        /**
@@ -566,7 +600,7 @@ class ActionController extends \TYPO3\CMS\Extbase\Mvc\Controller\AbstractControl
         * @return void
         */
        protected function clearCacheOnError() {
-               $extbaseSettings = $this->configurationManager->getConfiguration(\TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
+               $extbaseSettings = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
                if (isset($extbaseSettings['persistence']['enableAutomaticCacheClearing']) && $extbaseSettings['persistence']['enableAutomaticCacheClearing'] === '1') {
                        if (isset($GLOBALS['TSFE'])) {
                                $pageUid = $GLOBALS['TSFE']->id;
index 6f75aab..4e1aaa4 100644 (file)
@@ -6,20 +6,25 @@ namespace TYPO3\CMS\Fluid\View;
  *                                                                        *
  * It is free software; you can redistribute it and/or modify it under    *
  * the terms of the GNU Lesser General Public License, either version 3   *
- *  of the License, or (at your option) any later version.                *
+ * of the License, or (at your option) any later version.                 *
  *                                                                        *
  * The TYPO3 project - inspiring people to share!                         *
  *                                                                        */
 
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext;
+
 /**
  * The main template view. Should be used as view if you want Fluid Templating
  *
  * @api
  */
-class TemplateView extends \TYPO3\CMS\Fluid\View\AbstractTemplateView {
+class TemplateView extends AbstractTemplateView {
 
        /**
         * Pattern to be resolved for "@templateRoot" in the other patterns.
+        * Following placeholders are supported:
+        * - "@packageResourcesPath"
         *
         * @var string
         */
@@ -27,6 +32,8 @@ class TemplateView extends \TYPO3\CMS\Fluid\View\AbstractTemplateView {
 
        /**
         * Pattern to be resolved for "@partialRoot" in the other patterns.
+        * Following placeholders are supported:
+        * - "@packageResourcesPath"
         *
         * @var string
         */
@@ -34,34 +41,43 @@ class TemplateView extends \TYPO3\CMS\Fluid\View\AbstractTemplateView {
 
        /**
         * Pattern to be resolved for "@layoutRoot" in the other patterns.
+        * Following placeholders are supported:
+        * - "@packageResourcesPath"
         *
         * @var string
         */
        protected $layoutRootPathPattern = '@packageResourcesPath/Private/Layouts';
 
        /**
-        * Path to the template root. If NULL, then $this->templateRootPathPattern will be used.
+        * Path(s) to the template root. If NULL, then $this->templateRootPathPattern will be used.
         *
-        * @var string
+        * @var array
         */
-       protected $templateRootPath = NULL;
+       protected $templateRootPaths = NULL;
 
        /**
-        * Path to the partial root. If NULL, then $this->partialRootPathPattern will be used.
+        * Path(s) to the partial root. If NULL, then $this->partialRootPathPattern will be used.
         *
-        * @var string
+        * @var array
         */
-       protected $partialRootPath = NULL;
+       protected $partialRootPaths = NULL;
 
        /**
-        * Path to the layout root. If NULL, then $this->layoutRootPathPattern will be used.
+        * Path(s) to the layout root. If NULL, then $this->layoutRootPathPattern will be used.
         *
-        * @var string
+        * @var array
         */
-       protected $layoutRootPath = NULL;
+       protected $layoutRootPaths = NULL;
 
        /**
         * File pattern for resolving the template file
+        * Following placeholders are supported:
+        * - "@templateRoot"
+        * - "@partialRoot"
+        * - "@layoutRoot"
+        * - "@subpackage"
+        * - "@action"
+        * - "@format"
         *
         * @var string
         */
@@ -69,6 +85,13 @@ class TemplateView extends \TYPO3\CMS\Fluid\View\AbstractTemplateView {
 
        /**
         * Directory pattern for global partials. Not part of the public API, should not be changed for now.
+        * Following placeholders are supported:
+        * - "@templateRoot"
+        * - "@partialRoot"
+        * - "@layoutRoot"
+        * - "@subpackage"
+        * - "@partial"
+        * - "@format"
         *
         * @var string
         */
@@ -76,6 +99,13 @@ class TemplateView extends \TYPO3\CMS\Fluid\View\AbstractTemplateView {
 
        /**
         * File pattern for resolving the layout
+        * Following placeholders are supported:
+        * - "@templateRoot"
+        * - "@partialRoot"
+        * - "@layoutRoot"
+        * - "@subpackage"
+        * - "@layout"
+        * - "@format"
         *
         * @var string
         */
@@ -97,14 +127,13 @@ class TemplateView extends \TYPO3\CMS\Fluid\View\AbstractTemplateView {
 
        public function __construct() {
                $this->injectTemplateParser(\TYPO3\CMS\Fluid\Compatibility\TemplateParserBuilder::build());
-               $this->injectObjectManager(\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Extbase\\Object\\ObjectManager'));
+               $this->injectObjectManager(GeneralUtility::makeInstance('TYPO3\\CMS\\Extbase\\Object\\ObjectManager'));
                $this->setRenderingContext($this->objectManager->get('TYPO3\\CMS\\Fluid\\Core\\Rendering\\RenderingContextInterface'));
        }
 
        public function initializeView() {
        }
-
-       // Here, the backporter can insert a constructor method, which is needed for Fluid v4.
+       // Here, the backporter can insert a constructor method, which is needed for the TYPO3 CMS extension
 
        /**
         * Sets the path and name of of the template file. Effectively overrides the
@@ -130,32 +159,150 @@ class TemplateView extends \TYPO3\CMS\Fluid\View\AbstractTemplateView {
        }
 
        /**
-        * Checks whether a template can be resolved for the current request context.
+        * Set the root path to the templates.
+        * If set, overrides the one determined from $this->templateRootPathPattern
         *
-        * @param \TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext $controllerContext Controller context which is available inside the view
-        * @return boolean
+        * @param string $templateRootPath Root path to the templates. If set, overrides the one determined from $this->templateRootPathPattern
+        * @return void
         * @api
+        * @see setTemplateRootPaths()
         */
-       public function canRender(\TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext $controllerContext) {
-               $this->setControllerContext($controllerContext);
-               try {
-                       $this->getTemplateSource();
-                       return TRUE;
-               } catch (\TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException $e) {
-                       return FALSE;
+       public function setTemplateRootPath($templateRootPath) {
+               $this->setTemplateRootPaths(array($templateRootPath));
+       }
+
+       /**
+        * @return string Path to template root directory
+        * @deprecated since fluid 6.2, will be removed two versions later. Use getTemplateRootPaths() instead
+        */
+       protected function getTemplateRootPath() {
+               GeneralUtility::logDeprecatedFunction();
+               $templateRootPaths = $this->getTemplateRootPaths();
+               return array_shift($templateRootPaths);
+       }
+
+       /**
+        * Resolves the template root to be used inside other paths.
+        *
+        * @return array Path(s) to template root directory
+        */
+       public function getTemplateRootPaths() {
+               if ($this->templateRootPaths !== NULL) {
+                       return $this->templateRootPaths;
                }
+               /** @var $actionRequest \TYPO3\CMS\Extbase\Mvc\Request */
+               $actionRequest = $this->controllerContext->getRequest();
+               return array(str_replace('@packageResourcesPath', \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath($actionRequest->getControllerExtensionKey()) . 'Resources/', $this->templateRootPathPattern));
        }
 
        /**
-        * Set the root path to the templates.
+        * Set the root path(s) to the templates.
         * If set, overrides the one determined from $this->templateRootPathPattern
         *
-        * @param string $templateRootPath Root path to the templates. If set, overrides the one determined from $this->templateRootPathPattern
+        * @param array $templateRootPaths Root path(s) to the templates. If set, overrides the one determined from $this->templateRootPathPattern
         * @return void
         * @api
         */
-       public function setTemplateRootPath($templateRootPath) {
-               $this->templateRootPath = $templateRootPath;
+       public function setTemplateRootPaths(array $templateRootPaths) {
+               $this->templateRootPaths = $templateRootPaths;
+       }
+
+       /**
+        * Set the root path to the partials.
+        * If set, overrides the one determined from $this->partialRootPathPattern
+        *
+        * @param string $partialRootPath Root path to the partials. If set, overrides the one determined from $this->partialRootPathPattern
+        * @return void
+        * @api
+        * @see setPartialRootPaths()
+        */
+       public function setPartialRootPath($partialRootPath) {
+               $this->setPartialRootPaths(array($partialRootPath));
+       }
+
+       /**
+        * @return string Path to partial root directory
+        * @deprecated since fluid 6.2, will be removed two versions later. Use setPartialRootPaths() instead
+        */
+       protected function getPartialRootPath() {
+               GeneralUtility::logDeprecatedFunction();
+               $partialRootPaths = $this->getPartialRootPaths();
+               return array_shift($partialRootPaths);
+       }
+
+       /**
+        * Set the root path(s) to the partials.
+        * If set, overrides the one determined from $this->partialRootPathPattern
+        *
+        * @param array $partialRootPaths Root paths to the partials. If set, overrides the one determined from $this->partialRootPathPattern
+        * @return void
+        * @api
+        */
+       public function setPartialRootPaths(array $partialRootPaths) {
+               $this->partialRootPaths = $partialRootPaths;
+       }
+
+       /**
+        * Resolves the partial root to be used inside other paths.
+        *
+        * @return array Path(s) to partial root directory
+        */
+       protected function getPartialRootPaths() {
+               if ($this->partialRootPaths !== NULL) {
+                       return $this->partialRootPaths;
+               }
+               /** @var $actionRequest \TYPO3\CMS\Extbase\Mvc\Request */
+               $actionRequest = $this->controllerContext->getRequest();
+               return array(str_replace('@packageResourcesPath', \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath($actionRequest->getControllerExtensionKey()) . 'Resources/', $this->partialRootPathPattern));
+       }
+
+       /**
+        * Set the root path to the layouts.
+        * If set, overrides the one determined from $this->layoutRootPathPattern
+        *
+        * @param string $layoutRootPath Root path to the layouts. If set, overrides the one determined from $this->layoutRootPathPattern
+        * @return void
+        * @api
+        * @see setLayoutRootPaths()
+        */
+       public function setLayoutRootPath($layoutRootPath) {
+               $this->setLayoutRootPaths(array($layoutRootPath));
+       }
+
+       /**
+        * Set the root path(s) to the layouts.
+        * If set, overrides the one determined from $this->layoutRootPathPattern
+        *
+        * @param array $layoutRootPaths Root path to the layouts. If set, overrides the one determined from $this->layoutRootPathPattern
+        * @return void
+        * @api
+        */
+       public function setLayoutRootPaths(array $layoutRootPaths) {
+               $this->layoutRootPaths = $layoutRootPaths;
+       }
+
+       /**
+        * @return string Path to layout root directory
+        * @deprecated since fluid 6.2, will be removed two versions later. Use getLayoutRootPaths() instead
+        */
+       protected function getLayoutRootPath() {
+               GeneralUtility::logDeprecatedFunction();
+               $layoutRootPaths = $this->getLayoutRootPaths();
+               return array_shift($layoutRootPaths);
+       }
+
+       /**
+        * Resolves the layout root to be used inside other paths.
+        *
+        * @return string Path(s) to layout root directory
+        */
+       protected function getLayoutRootPaths() {
+               if ($this->layoutRootPaths !== NULL) {
+                       return $this->layoutRootPaths;
+               }
+               /** @var $actionRequest \TYPO3\CMS\Extbase\Mvc\Request */
+               $actionRequest = $this->controllerContext->getRequest();
+               return array(str_replace('@packageResourcesPath', \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath($actionRequest->getControllerExtensionKey()) . 'Resources/', $this->layoutRootPathPattern));
        }
 
        /**
@@ -168,7 +315,9 @@ class TemplateView extends \TYPO3\CMS\Fluid\View\AbstractTemplateView {
        protected function getTemplateIdentifier($actionName = NULL) {
                $templatePathAndFilename = $this->getTemplatePathAndFilename($actionName);
                if ($actionName === NULL) {
-                       $actionName = $this->controllerContext->getRequest()->getControllerActionName();
+                       /** @var $actionRequest \TYPO3\CMS\Extbase\Mvc\Request */
+                       $actionRequest = $this->controllerContext->getRequest();
+                       $actionName = $actionRequest->getControllerActionName();
                }
                $prefix = 'action_' . $actionName;
                return $this->createIdentifierForFile($templatePathAndFilename, $prefix);
@@ -180,13 +329,13 @@ class TemplateView extends \TYPO3\CMS\Fluid\View\AbstractTemplateView {
         *
         * @param string $actionName Name of the action. If NULL, will be taken from request.
         * @return string Full path to template
-        * @throws \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException
+        * @throws Exception\InvalidTemplateResourceException
         */
        protected function getTemplateSource($actionName = NULL) {
                $templatePathAndFilename = $this->getTemplatePathAndFilename($actionName);
                $templateSource = file_get_contents($templatePathAndFilename);
                if ($templateSource === FALSE) {
-                       throw new \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException('"' . $templatePathAndFilename . '" is not a valid template resource URI.', 1257246929);
+                       throw new Exception\InvalidTemplateResourceException('"' . $templatePathAndFilename . '" is not a valid template resource URI.', 1257246929);
                }
                return $templateSource;
        }
@@ -197,24 +346,27 @@ class TemplateView extends \TYPO3\CMS\Fluid\View\AbstractTemplateView {
         *
         * @param string $actionName Name of the action. If NULL, will be taken from request.
         * @return string Full path to template
-        * @throws \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException
+        * @throws Exception\InvalidTemplateResourceException
         */
        protected function getTemplatePathAndFilename($actionName = NULL) {
                if ($this->templatePathAndFilename !== NULL) {
                        return $this->templatePathAndFilename;
                }
                if ($actionName === NULL) {
-                       $actionName = $this->controllerContext->getRequest()->getControllerActionName();
+                       /** @var $actionRequest \TYPO3\CMS\Extbase\Mvc\Request */
+                       $actionRequest = $this->controllerContext->getRequest();
+                       $actionName = $actionRequest->getControllerActionName();
                }
                $actionName = ucfirst($actionName);
+
                $paths = $this->expandGenericPathPattern($this->templatePathAndFilenamePattern, FALSE, FALSE);
                foreach ($paths as &$templatePathAndFilename) {
-                       $templatePathAndFilename = str_replace('@action', $actionName, $templatePathAndFilename);
+                       $templatePathAndFilename = $this->resolveFileNamePath(str_replace('@action', $actionName, $templatePathAndFilename));
                        if (is_file($templatePathAndFilename)) {
                                return $templatePathAndFilename;
                        }
                }
-               throw new \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException('Template could not be loaded. I tried "' . implode('", "', $paths) . '"', 1225709595);
+               throw new Exception\InvalidTemplateResourceException('Template could not be loaded. I tried "' . implode('", "', $paths) . '"', 1225709595);
        }
 
        /**
@@ -240,13 +392,13 @@ class TemplateView extends \TYPO3\CMS\Fluid\View\AbstractTemplateView {
         *
         * @param string $layoutName Name of the layout to use. If none given, use "Default"
         * @return string contents of the layout template
-        * @throws \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException
+        * @throws Exception\InvalidTemplateResourceException
         */
        protected function getLayoutSource($layoutName = 'Default') {
                $layoutPathAndFilename = $this->getLayoutPathAndFilename($layoutName);
                $layoutSource = file_get_contents($layoutPathAndFilename);
                if ($layoutSource === FALSE) {
-                       throw new \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException('"' . $layoutPathAndFilename . '" is not a valid template resource URI.', 1257246929);
+                       throw new Exception\InvalidTemplateResourceException('"' . $layoutPathAndFilename . '" is not a valid template resource URI.', 1257246929);
                }
                return $layoutSource;
        }
@@ -261,7 +413,7 @@ class TemplateView extends \TYPO3\CMS\Fluid\View\AbstractTemplateView {
         *
         * @param string $layoutName Name of the layout to use. If none given, use "Default"
         * @return string Path and filename of layout files
-        * @throws \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException
+        * @throws Exception\InvalidTemplateResourceException
         */
        protected function getLayoutPathAndFilename($layoutName = 'Default') {
                if ($this->layoutPathAndFilename !== NULL) {
@@ -270,12 +422,12 @@ class TemplateView extends \TYPO3\CMS\Fluid\View\AbstractTemplateView {
                $paths = $this->expandGenericPathPattern($this->layoutPathAndFilenamePattern, TRUE, TRUE);
                $layoutName = ucfirst($layoutName);
                foreach ($paths as &$layoutPathAndFilename) {
-                       $layoutPathAndFilename = str_replace('@layout', $layoutName, $layoutPathAndFilename);
+                       $layoutPathAndFilename = $this->resolveFileNamePath(str_replace('@layout', $layoutName, $layoutPathAndFilename));
                        if (is_file($layoutPathAndFilename)) {
                                return $layoutPathAndFilename;
                        }
                }
-               throw new \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException('The template files "' . implode('", "', $paths) . '" could not be loaded.', 1225709595);
+               throw new Exception\InvalidTemplateResourceException('The template files "' . implode('", "', $paths) . '" could not be loaded.', 1225709595);
        }
 
        /**
@@ -296,13 +448,13 @@ class TemplateView extends \TYPO3\CMS\Fluid\View\AbstractTemplateView {
         *
         * @param string $partialName The name of the partial
         * @return string contents of the partial template
-        * @throws \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException
+        * @throws Exception\InvalidTemplateResourceException
         */
        protected function getPartialSource($partialName) {
                $partialPathAndFilename = $this->getPartialPathAndFilename($partialName);
                $partialSource = file_get_contents($partialPathAndFilename);
                if ($partialSource === FALSE) {
-                       throw new \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException('"' . $partialPathAndFilename . '" is not a valid template resource URI.', 1257246929);
+                       throw new Exception\InvalidTemplateResourceException('"' . $partialPathAndFilename . '" is not a valid template resource URI.', 1257246929);
                }
                return $partialSource;
        }
@@ -312,84 +464,45 @@ class TemplateView extends \TYPO3\CMS\Fluid\View\AbstractTemplateView {
         *
         * @param string $partialName The name of the partial
         * @return string the full path which should be used. The path definitely exists.
-        * @throws \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException
+        * @throws Exception\InvalidTemplateResourceException
         */
        protected function getPartialPathAndFilename($partialName) {
                $paths = $this->expandGenericPathPattern($this->partialPathAndFilenamePattern, TRUE, TRUE);
                foreach ($paths as &$partialPathAndFilename) {
-                       $partialPathAndFilename = str_replace('@partial', $partialName, $partialPathAndFilename);
-                       if (is_file($partialPathAndFilename)) {
+                       $partialPathAndFilename = $this->resolveFileNamePath(str_replace('@partial', $partialName, $partialPathAndFilename));
+                       if (@file_exists($partialPathAndFilename)) {
                                return $partialPathAndFilename;
                        }
                }
-               throw new \TYPO3\CMS\Fluid\View\Exception\InvalidTemplateResourceException('The template files "' . implode('", "', $paths) . '" could not be loaded.', 1225709595);
-       }
-
-       /**
-        * Resolves the template root to be used inside other paths.
-        *
-        * @return string Path to template root directory
-        */
-       protected function getTemplateRootPath() {
-               if ($this->templateRootPath !== NULL) {
-                       return $this->templateRootPath;
-               } else {
-                       return str_replace('@packageResourcesPath', \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath($this->controllerContext->getRequest()->getControllerExtensionKey()) . 'Resources/', $this->templateRootPathPattern);
-               }
+               throw new Exception\InvalidTemplateResourceException('The template files "' . implode('", "', $paths) . '" could not be loaded.', 1225709595);
        }
 
        /**
-        * Set the root path to the partials.
-        * If set, overrides the one determined from $this->partialRootPathPattern
+        * Checks whether a template can be resolved for the current request context.
         *
-        * @param string $partialRootPath Root path to the partials. If set, overrides the one determined from $this->partialRootPathPattern
-        * @return void
+        * @param ControllerContext $controllerContext Controller context which is available inside the view
+        * @return boolean
         * @api
         */
-       public function setPartialRootPath($partialRootPath) {
-               $this->partialRootPath = $partialRootPath;
-       }
-
-       /**
-        * Resolves the partial root to be used inside other paths.
-        *
-        * @return string Path to partial root directory
-        */
-       protected function getPartialRootPath() {
-               if ($this->partialRootPath !== NULL) {
-                       return $this->partialRootPath;
-               } else {
-                       return str_replace('@packageResourcesPath', \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath($this->controllerContext->getRequest()->getControllerExtensionKey()) . 'Resources/', $this->partialRootPathPattern);
+       public function canRender(ControllerContext $controllerContext) {
+               $this->setControllerContext($controllerContext);
+               try {
+                       $this->getTemplateSource();
+                       return TRUE;
+               } catch (Exception\InvalidTemplateResourceException $e) {
+                       return FALSE;
                }
        }
 
        /**
-        * Set the root path to the layouts.
-        * If set, overrides the one determined from $this->layoutRootPathPattern
+        * Processes following placeholders inside $pattern:
+        *  - "@templateRoot"
+        *  - "@partialRoot"
+        *  - "@layoutRoot"
+        *  - "@subpackage"
+        *  - "@controller"
+        *  - "@format"
         *
-        * @param string $layoutRootPath Root path to the layouts. If set, overrides the one determined from $this->layoutRootPathPattern
-        * @return void
-        * @api
-        */
-       public function setLayoutRootPath($layoutRootPath) {
-               $this->layoutRootPath = $layoutRootPath;
-       }
-
-       /**
-        * Resolves the layout root to be used inside other paths.
-        *
-        * @return string Path to layout root directory
-        */
-       protected function getLayoutRootPath() {
-               if ($this->layoutRootPath !== NULL) {
-                       return $this->layoutRootPath;
-               } else {
-                       return str_replace('@packageResourcesPath', \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath($this->controllerContext->getRequest()->getControllerExtensionKey()) . 'Resources/', $this->layoutRootPathPattern);
-               }
-       }
-
-       /**
-        * Processes "@templateRoot", "@subpackage", "@controller", and "@format" placeholders inside $pattern.
         * This method is used to generate "fallback chains" for file system locations where a certain Partial can reside.
         *
         * If $bubbleControllerAndSubpackage is FALSE and $formatIsOptional is FALSE, then the resulting array will only have one element
@@ -414,44 +527,75 @@ class TemplateView extends \TYPO3\CMS\Fluid\View\AbstractTemplateView {
         * @param string $pattern Pattern to be resolved
         * @param boolean $bubbleControllerAndSubpackage if TRUE, then we successively split off parts from "@controller" and "@subpackage" until both are empty.
         * @param boolean $formatIsOptional if TRUE, then half of the resulting strings will have ."@format" stripped off, and the other half will have it.
-        * @return array unix style path
+        * @return array unix style paths
         */
        protected function expandGenericPathPattern($pattern, $bubbleControllerAndSubpackage, $formatIsOptional) {
-               $pattern = str_replace('@templateRoot', $this->getTemplateRootPath(), $pattern);
-               $pattern = str_replace('@partialRoot', $this->getPartialRootPath(), $pattern);
-               $pattern = str_replace('@layoutRoot', $this->getLayoutRootPath(), $pattern);
-
-               $subpackageKey = $this->controllerContext->getRequest()->getControllerSubpackageKey();
-               $controllerName = $this->controllerContext->getRequest()->getControllerName();
+               $paths = array($pattern);
+               $this->expandPatterns($paths, '@templateRoot', $this->getTemplateRootPaths());
+               $this->expandPatterns($paths, '@partialRoot', $this->getPartialRootPaths());
+               $this->expandPatterns($paths, '@layoutRoot', $this->getLayoutRootPaths());
+
+               /** @var $actionRequest \TYPO3\CMS\Extbase\Mvc\Request */
+               $actionRequest = $this->controllerContext->getRequest();
+               $subpackageKey = $actionRequest->getControllerSubpackageKey();
+               $controllerName = $actionRequest->getControllerName();
                if ($subpackageKey !== NULL) {
                        if (strpos($subpackageKey, \TYPO3\CMS\Fluid\Fluid::NAMESPACE_SEPARATOR) !== FALSE) {
                                $namespaceSeparator = \TYPO3\CMS\Fluid\Fluid::NAMESPACE_SEPARATOR;
                        } else {
                                $namespaceSeparator = \TYPO3\CMS\Fluid\Fluid::LEGACY_NAMESPACE_SEPARATOR;
                        }
-                       $subpackageParts = explode($namespaceSeparator, $subpackageKey);
+                       $subpackageKeyParts = explode($namespaceSeparator, $subpackageKey);
                } else {
-                       $subpackageParts = array();
+                       $subpackageKeyParts = array();
                }
-               $results = array();
-
-               $i = ($controllerName === NULL) ? 0 : -1;
-               do {
-                       $temporaryPattern = $pattern;
-                       if ($i < 0) {
-                               $temporaryPattern = str_replace('@controller', $controllerName, $temporaryPattern);
-                       } else {
-                               $temporaryPattern = str_replace('//', '/', str_replace('@controller', '', $temporaryPattern));
+               if ($bubbleControllerAndSubpackage) {
+                       $numberOfPathsBeforeSubpackageExpansion = count($paths);
+                       $numberOfSubpackageParts = count($subpackageKeyParts);
+                       $subpackageReplacements = array();
+                       for ($i = 0; $i <= $numberOfSubpackageParts; $i++) {
+                               $subpackageReplacements[] = implode('/', ($i < 0 ? $subpackageKeyParts : array_slice($subpackageKeyParts, $i)));
                        }
-                       $temporaryPattern = str_replace('@subpackage', implode('/', ($i < 0 ? $subpackageParts : array_slice($subpackageParts, $i))), $temporaryPattern);
+                       $this->expandPatterns($paths, '@subpackage', $subpackageReplacements);
 
-                       $results[] = \TYPO3\CMS\Core\Utility\GeneralUtility::fixWindowsFilePath(str_replace('@format', $this->controllerContext->getRequest()->getFormat(), $temporaryPattern));
-                       if ($formatIsOptional) {
-                               $results[] = \TYPO3\CMS\Core\Utility\GeneralUtility::fixWindowsFilePath(str_replace('.@format', '', $temporaryPattern));
+                       for ($i = ($numberOfPathsBeforeSubpackageExpansion - 1) * ($numberOfSubpackageParts + 1); $i >= 0; $i -= ($numberOfSubpackageParts + 1)) {
+                               array_splice($paths, $i, 0, str_replace('@controller', $controllerName, $paths[$i]));
                        }
-               } while ($i++ < count($subpackageParts) && $bubbleControllerAndSubpackage);
+                       $this->expandPatterns($paths, '@controller', array(''));
+               } else {
+                       $i = $controllerName === NULL ? 0 : -1;
+                       $this->expandPatterns($paths, '@subpackage', array(implode('/', $i < 0 ? $subpackageKeyParts :
+                               array_slice($subpackageKeyParts, $i))));
+                       $this->expandPatterns($paths, '@controller', array($controllerName));
+               }
 
-               return $results;
+               if ($formatIsOptional) {
+                       $this->expandPatterns($paths, '.@format', array('.' . $actionRequest->getFormat(), ''));
+                       $this->expandPatterns($paths, '@format', array($actionRequest->getFormat(), ''));
+               } else {
+                       $this->expandPatterns($paths, '.@format', array('.' . $actionRequest->getFormat()));
+                       $this->expandPatterns($paths, '@format', array($actionRequest->getFormat()));
+               }
+               return array_values(array_unique($paths));
+       }
+
+       /**
+        * Expands the given $patterns by adding an array element for each $replacement
+        * replacing occurrences of $search.
+        *
+        * @param array $patterns
+        * @param string $search
+        * @param array $replacements
+        * @return void
+        */
+       protected function expandPatterns(array &$patterns, $search, array $replacements) {
+               $patternsWithReplacements = array();
+               foreach ($patterns as $pattern) {
+                       foreach ($replacements as $replacement) {
+                               $patternsWithReplacements[] = GeneralUtility::fixWindowsFilePath(str_replace($search, $replacement, $pattern));
+                       }
+               }
+               $patterns = $patternsWithReplacements;
        }
 
        /**
@@ -464,15 +608,27 @@ class TemplateView extends \TYPO3\CMS\Fluid\View\AbstractTemplateView {
         * @return string
         */
        protected function createIdentifierForFile($pathAndFilename, $prefix) {
-               $request = $this->controllerContext->getRequest();
-               $extensionName = $request->getControllerExtensionName();
-               $subPackageKey = $request->getControllerSubpackageKey();
+               /** @var $actionRequest \TYPO3\CMS\Extbase\Mvc\Request */
+               $actionRequest = $this->controllerContext->getRequest();
+               $extensionName = $actionRequest->getControllerExtensionName();
+               $subPackageKey = $actionRequest->getControllerSubpackageKey();
                if ($subPackageKey !== NULL) {
                        $extensionName .= '_' . $subPackageKey;
                }
-               $controllerName = $request->getControllerName();
+               $controllerName = $actionRequest->getControllerName();
                $templateModifiedTimestamp = filemtime($pathAndFilename);
                $templateIdentifier = sprintf('%s_%s_%s_%s', $extensionName, $controllerName, $prefix, sha1($pathAndFilename . '|' . $templateModifiedTimestamp));
                return $templateIdentifier;
        }
-}
+
+       /**
+        * Wrapper method to make the static call to GeneralUtility mockable in tests
+        *
+        * @param string $pathAndFilename
+        *
+        * @return string absolute pathAndFilename
+        */
+       protected function resolveFileNamePath($pathAndFilename) {
+               return GeneralUtility::getFileAbsFileName($pathAndFilename);
+       }
+}
\ No newline at end of file
index 6151ab4..ece6dce 100644 (file)
@@ -6,23 +6,26 @@ namespace TYPO3\CMS\Fluid\Tests\Unit\View;
  *                                                                        *
  * It is free software; you can redistribute it and/or modify it under    *
  * the terms of the GNU Lesser General Public License, either version 3   *
- *  of the License, or (at your option) any later version.                *
+ * of the License, or (at your option) any later version.                 *
  *                                                                        *
  * The TYPO3 project - inspiring people to share!                         *
  *                                                                        */
 
-use \org\bovigo\vfs\vfsStreamDirectory;
-use \org\bovigo\vfs\vfsStreamWrapper;
-
 include_once(__DIR__ . '/Fixtures/TransparentSyntaxTreeNode.php');
 include_once(__DIR__ . '/Fixtures/TemplateViewFixture.php');
 
+use org\bovigo\vfs\vfsStreamWrapper;
+use org\bovigo\vfs\vfsStreamDirectory;
+use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
+
 /**
  * Testcase for the TemplateView
  */
 class TemplateViewTest extends \TYPO3\CMS\Extbase\Tests\Unit\BaseTestCase {
 
        /**
+        * Test for #42123
+        * "Widgets with underscores in class names do not work because the subpackage key is not handled correctly."
         * @test
         */
        public function expandGenericPathPatternWorksWithOldNamingSchemeOfSubPackage() {
@@ -30,12 +33,14 @@ class TemplateViewTest extends \TYPO3\CMS\Extbase\Tests\Unit\BaseTestCase {
                $templateView = $this->getAccessibleMock('TYPO3\\CMS\\Fluid\\View\\TemplateView', array('getTemplateRootPath', 'getPartialRootPath', 'getLayoutRootPath'), array(), '', FALSE);
                $templateView->_set('controllerContext', $mockControllerContext);
                $templateView->expects($this->any())->method('getTemplateRootPath')->will($this->returnValue('Resources/Private/'));
-               $expected = array('Resources/Private/Templates/ViewHelpers/Widget/Paginate/@action.html');
-               $actual = $templateView->_call('expandGenericPathPattern', '@templateRoot/Templates/@subpackage/@controller/@action.@format', FALSE, FALSE);
+               $expected = array(ExtensionManagementUtility::extPath('frontend') . 'Resources/Private/Templates/ViewHelpers/Widget/Paginate/@action.html');
+               $actual = $templateView->_call('expandGenericPathPattern', '@templateRoot/@subpackage/@controller/@action.@format', FALSE, FALSE);
                $this->assertEquals($expected, $actual);
        }
 
        /**
+        * Test for #42123
+        * "Widgets with underscores in class names do not work because the subpackage key is not handled correctly."
         * @test
         */
        public function expandGenericPathPatternWorksWithNewNamingSchemeOfSubPackage() {
@@ -43,20 +48,484 @@ class TemplateViewTest extends \TYPO3\CMS\Extbase\Tests\Unit\BaseTestCase {
                $templateView = $this->getAccessibleMock('TYPO3\\CMS\\Fluid\\View\\TemplateView', array('getTemplateRootPath', 'getPartialRootPath', 'getLayoutRootPath'), array(), '', FALSE);
                $templateView->_set('controllerContext', $mockControllerContext);
                $templateView->expects($this->any())->method('getTemplateRootPath')->will($this->returnValue('Resources/Private/'));
-               $expected = array('Resources/Private/Templates/ViewHelpers/Widget/Paginate/@action.html');
-               $actual = $templateView->_call('expandGenericPathPattern', '@templateRoot/Templates/@subpackage/@controller/@action.@format', FALSE, FALSE);
+               $expected = array(ExtensionManagementUtility::extPath('frontend') . 'Resources/Private/Templates/ViewHelpers/Widget/Paginate/@action.html');
+               $actual = $templateView->_call('expandGenericPathPattern', '@templateRoot/@subpackage/@controller/@action.@format', FALSE, FALSE);
                $this->assertEquals($expected, $actual);
        }
 
        /**
+        * Helper to build mock controller context needed to test expandGenericPathPattern.
+        *
+        * @param string $packageKey
+        * @param string $subPackageKey
+        * @param string $controllerName
+        * @param string $format
+        * @return \TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext
+        */
+       protected function setupMockControllerContextForPathResolving($packageKey, $subPackageKey, $controllerName, $format) {
+               $controllerObjectName = "TYPO3\\$packageKey\\" . ($subPackageKey != $subPackageKey . '\\' ? : '') . 'Controller\\' . $controllerName . 'Controller';
+               $mockRequest = $this->getMock('TYPO3\\CMS\\Extbase\\Mvc\\Web\\Request');
+               $mockRequest->expects($this->any())->method('getControllerExtensionKey')->will($this->returnValue('frontend'));
+               $mockRequest->expects($this->any())->method('getControllerPackageKey')->will($this->returnValue($packageKey));
+               $mockRequest->expects($this->any())->method('getControllerSubPackageKey')->will($this->returnValue($subPackageKey));
+               $mockRequest->expects($this->any())->method('getControllerName')->will($this->returnValue($controllerName));
+               $mockRequest->expects($this->any())->method('getControllerObjectName')->will($this->returnValue($controllerObjectName));
+               $mockRequest->expects($this->any())->method('getFormat')->will($this->returnValue($format));
+
+               $mockControllerContext = $this->getMock('TYPO3\\CMS\\Extbase\\Mvc\\Controller\\ControllerContext', array('getRequest'), array(), '', FALSE);
+               $mockControllerContext->expects($this->any())->method('getRequest')->will($this->returnValue($mockRequest));
+
+               return $mockControllerContext;
+       }
+
+       public function expandGenericPathPatternDataProvider() {
+               return array(
+                       // bubbling controller & subpackage parts and optional format
+                       array(
+                               'package' => 'Some.Package',
+                               'subPackage' => 'Some\\Sub\\Package',
+                               'controller' => 'SomeController',
+                               'format' => 'html',
+                               'templateRootPath' => 'Resources/Private/Templates',
+                               'templateRootPaths' => NULL,
+                               'partialRootPath' => 'Resources/Private/Partials',
+                               'partialRootPaths' => NULL,
+                               'layoutRootPath' => 'Resources/Private/Layouts',
+                               'layoutRootPaths' => NULL,
+                               'bubbleControllerAndSubpackage' => TRUE,
+                               'formatIsOptional' => TRUE,
+                               'pattern' => '@templateRoot/@subpackage/@controller/@action.@format',
+                               'expectedResult' => array(
+                                       'Resources/Private/Templates/Some/Sub/Package/SomeController/@action.html',
+                                       'Resources/Private/Templates/Some/Sub/Package/SomeController/@action',
+                                       'Resources/Private/Templates/Some/Sub/Package/@action.html',
+                                       'Resources/Private/Templates/Some/Sub/Package/@action',
+                                       'Resources/Private/Templates/Sub/Package/@action.html',
+                                       'Resources/Private/Templates/Sub/Package/@action',
+                                       'Resources/Private/Templates/Package/@action.html',
+                                       'Resources/Private/Templates/Package/@action',
+                                       'Resources/Private/Templates/@action.html',
+                                       'Resources/Private/Templates/@action',
+                               )
+                       ),
+                       // just optional format
+                       array(
+                               'package' => 'Some.Package',
+                               'subPackage' => 'Some\\Sub\\Package',
+                               'controller' => 'SomeController',
+                               'format' => 'html',
+                               'templateRootPath' => 'Resources/Private/Templates/',
+                               'templateRootPaths' => NULL,
+                               'partialRootPath' => 'Resources/Private/Partials',
+                               'partialRootPaths' => NULL,
+                               'layoutRootPath' => 'Resources/Private/Layouts',
+                               'layoutRootPaths' => NULL,
+                               'bubbleControllerAndSubpackage' => FALSE,
+                               'formatIsOptional' => TRUE,
+                               'pattern' => '@templateRoot/@subpackage/@controller/@action.@format',
+                               'expectedResult' => array(
+                                       'Resources/Private/Templates/Some/Sub/Package/SomeController/@action.html',
+                                       'Resources/Private/Templates/Some/Sub/Package/SomeController/@action',
+                               )
+                       ),
+                       // just bubbling controller & subpackage parts
+                       array(
+                               'package' => 'Some.Package',
+                               'subPackage' => 'Some\\Sub\\Package',
+                               'controller' => 'SomeController',
+                               'format' => 'json',
+                               'templateRootPath' => 'Resources/Private/Templates',
+                               'templateRootPaths' => NULL,
+                               'partialRootPath' => 'Resources/Private/Partials',
+                               'partialRootPaths' => NULL,
+                               'layoutRootPath' => 'Resources/Private/Layouts',
+                               'layoutRootPaths' => NULL,
+                               'bubbleControllerAndSubpackage' => TRUE,
+                               'formatIsOptional' => FALSE,
+                               'pattern' => '@partialRoot/@subpackage/@controller/@action.@format',
+                               'expectedResult' => array(
+                                       'Resources/Private/Partials/Some/Sub/Package/SomeController/@action.json',
+                                       'Resources/Private/Partials/Some/Sub/Package/@action.json',
+                                       'Resources/Private/Partials/Sub/Package/@action.json',
+                                       'Resources/Private/Partials/Package/@action.json',
+                                       'Resources/Private/Partials/@action.json',
+                               )
+                       ),
+                       // layoutRootPath
+                       array(
+                               'package' => 'Some.Package',
+                               'subPackage' => NULL,
+                               'controller' => NULL,
+                               'format' => 'xml',
+                               'templateRootPath' => 'Resources/Private/Templates',
+                               'templateRootPaths' => NULL,
+                               'partialRootPath' => 'Resources/Private/Partials',
+                               'partialRootPaths' => NULL,
+                               'layoutRootPath' => 'Resources/Private/Layouts',
+                               'layoutRootPaths' => NULL,
+                               'bubbleControllerAndSubpackage' => TRUE,
+                               'formatIsOptional' => TRUE,
+                               'pattern' => '@layoutRoot/@subpackage/@controller/@action.@format',
+                               'expectedResult' => array(
+                                       'Resources/Private/Layouts/@action.xml',
+                                       'Resources/Private/Layouts/@action',
+                               )
+                       ),
+                       // partialRootPath
+                       array(
+                               'package' => 'Some.Package',
+                               'subPackage' => 'Some\\Sub\\Package',
+                               'controller' => NULL,
+                               'format' => 'html',
+                               'templateRootPath' => 'Resources/Private/Templates',
+                               'templateRootPaths' => NULL,
+                               'partialRootPath' => 'Resources/Private/Partials',
+                               'partialRootPaths' => NULL,
+                               'layoutRootPath' => 'Resources/Private/Layouts',
+                               'layoutRootPaths' => NULL,
+                               'bubbleControllerAndSubpackage' => TRUE,
+                               'formatIsOptional' => TRUE,
+                               'pattern' => '@templateRoot/@subpackage/@controller/@action.@format',
+                               'expectedResult' => array(
+                                       'Resources/Private/Templates/Some/Sub/Package/@action.html',
+                                       'Resources/Private/Templates/Some/Sub/Package/@action',
+                                       'Resources/Private/Templates/Sub/Package/@action.html',
+                                       'Resources/Private/Templates/Sub/Package/@action',
+                                       'Resources/Private/Templates/Package/@action.html',
+                                       'Resources/Private/Templates/Package/@action',
+                                       'Resources/Private/Templates/@action.html',
+                                       'Resources/Private/Templates/@action',
+                               )
+                       ),
+                       // optional format as directory name
+                       array(
+                               'package' => 'Some.Package',
+                               'subPackage' => 'Some\\Sub\\Package',
+                               'controller' => 'SomeController',
+                               'format' => 'xml',
+                               'templateRootPath' => 'Resources/Private/Templates_@format',
+                               'templateRootPaths' => NULL,
+                               'partialRootPath' => 'Resources/Private/Partials',
+                               'partialRootPaths' => NULL,
+                               'layoutRootPath' => 'Resources/Private/Layouts',
+                               'layoutRootPaths' => NULL,
+                               'bubbleControllerAndSubpackage' => FALSE,
+                               'formatIsOptional' => TRUE,
+                               'pattern' => '@templateRoot/@subpackage/@controller/@action',
+                               'expectedResult' => array(
+                                       'Resources/Private/Templates_xml/Some/Sub/Package/SomeController/@action',
+                                       'Resources/Private/Templates_/Some/Sub/Package/SomeController/@action',
+                               )
+                       ),
+                       // mandatory format as directory name
+                       array(
+                               'package' => 'Some.Package',
+                               'subPackage' => 'Some\\Sub\\Package',
+                               'controller' => 'SomeController',
+                               'format' => 'json',
+                               'templateRootPath' => 'Resources/Private/Templates_@format',
+                               'templateRootPaths' => NULL,
+                               'partialRootPath' => 'Resources/Private/Partials',
+                               'partialRootPaths' => NULL,
+                               'layoutRootPath' => 'Resources/Private/Layouts',
+                               'layoutRootPaths' => NULL,
+                               'bubbleControllerAndSubpackage' => FALSE,
+                               'formatIsOptional' => FALSE,
+                               'pattern' => '@templateRoot/@subpackage/@controller/@action',
+                               'expectedResult' => array(
+                                       'Resources/Private/Templates_json/Some/Sub/Package/SomeController/@action',
+                               )
+                       ),
+                       // paths must not contain double slashes
+                       array(
+                               'package' => 'Some.Package',
+                               'subPackage' => NULL,
+                               'controller' => 'SomeController',
+                               'format' => 'html',
+                               'templateRootPath' => 'Resources/Private/Templates',
+                               'templateRootPaths' => NULL,
+                               'partialRootPath' => 'Resources/Private/Partials',
+                               'partialRootPaths' => NULL,
+                               'layoutRootPath' => 'Some/Root/Path/',
+                               'layoutRootPaths' => NULL,
+                               'bubbleControllerAndSubpackage' => TRUE,
+                               'formatIsOptional' => TRUE,
+                               'pattern' => '@layoutRoot/@subpackage/@controller/@action.@format',
+                               'expectedResult' => array(
+                                       'Some/Root/Path/SomeController/@action.html',
+                                       'Some/Root/Path/SomeController/@action',
+                                       'Some/Root/Path/@action.html',
+                                       'Some/Root/Path/@action',
+                               )
+                       ),
+                       // paths must be unique
+                       array(
+                               'package' => 'Some.Package',
+                               'subPackage' => 'Some\\Sub\\Package',
+                               'controller' => 'SomeController',
+                               'format' => 'json',
+                               'templateRootPath' => 'Resources/Private/Templates',
+                               'templateRootPaths' => NULL,
+                               'partialRootPath' => 'Resources/Private/Partials',
+                               'partialRootPaths' => NULL,
+                               'layoutRootPath' => 'Resources/Private/Layouts',
+                               'layoutRootPaths' => NULL,
+                               'bubbleControllerAndSubpackage' => TRUE,
+                               'formatIsOptional' => FALSE,
+                               'pattern' => 'foo',
+                               'expectedResult' => array(
+                                       'foo',
+                               )
+                       ),
+                       // template fallback paths
+                       array(
+                               'package' => 'Some.Package',
+                               'subPackage' => 'Some\\Sub\\Package',
+                               'controller' => 'SomeController',
+                               'format' => 'html',
+                               'templateRootPath' => 'Resources/Private/Templates',
+                               'templateRootPaths' => array('Resources/Private/Templates', 'Some/Fallback/Path'),
+                               'partialRootPath' => 'Resources/Private/Partials',
+                               'partialRootPaths' => NULL,
+                               'layoutRootPath' => 'Resources/Private/Layouts',
+                               'layoutRootPaths' => NULL,
+                               'bubbleControllerAndSubpackage' => FALSE,
+                               'formatIsOptional' => TRUE,
+                               'pattern' => '@templateRoot/@subpackage/@controller/@action.@format',
+                               'expectedResult' => array(
+                                       'Resources/Private/Templates/Some/Sub/Package/SomeController/@action.html',
+                                       'Resources/Private/Templates/Some/Sub/Package/SomeController/@action',
+                                       'Some/Fallback/Path/Some/Sub/Package/SomeController/@action.html',
+                                       'Some/Fallback/Path/Some/Sub/Package/SomeController/@action',
+                               )
+                       ),
+                       // template fallback paths with bubbleControllerAndSubpackage
+                       array(
+                               'package' => 'Some.Package',
+                               'subPackage' => 'Some\\Sub\\Package',
+                               'controller' => 'SomeController',
+                               'format' => 'html',
+                               'templateRootPath' => 'Resources/Private/Templates',
+                               'templateRootPaths' => array('Resources/Private/Templates', 'Some/Fallback/Path'),
+                               'partialRootPath' => 'Resources/Private/Partials',
+                               'partialRootPaths' => NULL,
+                               'layoutRootPath' => 'Resources/Private/Layouts',
+                               'layoutRootPaths' => NULL,
+                               'bubbleControllerAndSubpackage' => TRUE,
+                               'formatIsOptional' => FALSE,
+                               'pattern' => '@templateRoot/@subpackage/@controller/@action.@format',
+                               'expectedResult' => array(
+                                       'Resources/Private/Templates/Some/Sub/Package/SomeController/@action.html',
+                                       'Resources/Private/Templates/Some/Sub/Package/@action.html',
+                                       'Resources/Private/Templates/Sub/Package/@action.html',
+                                       'Resources/Private/Templates/Package/@action.html',
+                                       'Resources/Private/Templates/@action.html',
+                                       'Some/Fallback/Path/Some/Sub/Package/SomeController/@action.html',
+                                       'Some/Fallback/Path/Some/Sub/Package/@action.html',
+                                       'Some/Fallback/Path/Sub/Package/@action.html',
+                                       'Some/Fallback/Path/Package/@action.html',
+                                       'Some/Fallback/Path/@action.html',
+                               )
+                       ),
+                       // partial fallback paths
+                       array(
+                               'package' => 'Some.Package',
+                               'subPackage' => 'Some\\Sub\\Package',
+                               'controller' => 'SomeController',
+                               'format' => 'html',
+                               'templateRootPath' => 'Resources/Private/Templates',
+                               'templateRootPaths' => NULL,
+                               'partialRootPath' => 'Resources/Private/Partials',
+                               'partialRootPaths' => array('Default/Resources/Path', 'Fallback/'),
+                               'layoutRootPath' => 'Resources/Private/Layouts',
+                               'layoutRootPaths' => NULL,
+                               'bubbleControllerAndSubpackage' => FALSE,
+                               'formatIsOptional' => TRUE,
+                               'pattern' => '@partialRoot/@subpackage/@controller/@partial.@format',
+                               'expectedResult' => array(
+                                       'Default/Resources/Path/Some/Sub/Package/SomeController/@partial.html',
+                                       'Default/Resources/Path/Some/Sub/Package/SomeController/@partial',
+                                       'Fallback/Some/Sub/Package/SomeController/@partial.html',
+                                       'Fallback/Some/Sub/Package/SomeController/@partial',
+                               )
+                       ),
+                       // partial fallback paths with bubbleControllerAndSubpackage
+                       array(
+                               'package' => 'Some.Package',
+                               'subPackage' => 'Some\\Sub\\Package',
+                               'controller' => 'SomeController',
+                               'format' => 'html',
+                               'templateRootPath' => 'Resources/Private/Templates',
+                               'templateRootPaths' => array('Resources/Private/Templates', 'Some/Fallback/Path'),
+                               'partialRootPath' => 'Resources/Private/Partials',
+                               'partialRootPaths' => array('Default/Resources/Path', 'Fallback1/', 'Fallback2'),
+                               'layoutRootPath' => 'Resources/Private/Layouts',
+                               'layoutRootPaths' => NULL,
+                               'bubbleControllerAndSubpackage' => TRUE,
+                               'formatIsOptional' => TRUE,
+                               'pattern' => '@partialRoot/@controller/@subpackage/@partial',
+                               'expectedResult' => array(
+                                       'Default/Resources/Path/SomeController/Some/Sub/Package/@partial',
+                                       'Default/Resources/Path/Some/Sub/Package/@partial',
+                                       'Default/Resources/Path/Sub/Package/@partial',
+                                       'Default/Resources/Path/Package/@partial',
+                                       'Default/Resources/Path/@partial',
+                                       'Fallback1/SomeController/Some/Sub/Package/@partial',
+                                       'Fallback1/Some/Sub/Package/@partial',
+                                       'Fallback1/Sub/Package/@partial',
+                                       'Fallback1/Package/@partial',
+                                       'Fallback1/@partial',
+                                       'Fallback2/SomeController/Some/Sub/Package/@partial',
+                                       'Fallback2/Some/Sub/Package/@partial',
+                                       'Fallback2/Sub/Package/@partial',
+                                       'Fallback2/Package/@partial',
+                                       'Fallback2/@partial',
+                               )
+                       ),
+                       // layout fallback paths
+                       array(
+                               'package' => 'Some.Package',
+                               'subPackage' => 'Some\\Sub\\Package',
+                               'controller' => 'SomeController',
+                               'format' => 'html',
+                               'templateRootPath' => 'Resources/Private/Templates',
+                               'templateRootPaths' => array('Resources/Private/Templates', 'Some/Fallback/Path'),
+                               'partialRootPath' => 'Resources/Private/Partials',
+                               'partialRootPaths' => array('foo', 'bar'),
+                               'layoutRootPath' => 'Resources/Private/Layouts',
+                               'layoutRootPaths' => array('Default/Layout/Path', 'Fallback/Path'),
+                               'bubbleControllerAndSubpackage' => FALSE,
+                               'formatIsOptional' => FALSE,
+                               'pattern' => '@layoutRoot/@subpackage/@controller/@layout.@format',
+                               'expectedResult' => array(
+                                       'Default/Layout/Path/Some/Sub/Package/SomeController/@layout.html',
+                                       'Fallback/Path/Some/Sub/Package/SomeController/@layout.html',
+                               )
+                       ),
+                       // layout fallback paths with bubbleControllerAndSubpackage
+                       array(
+                               'package' => 'Some.Package',
+                               'subPackage' => 'Some\\Sub\\Package',
+                               'controller' => 'SomeController',
+                               'format' => 'html',
+                               'templateRootPath' => 'Resources/Private/Templates',
+                               'templateRootPaths' => NULL,
+                               'partialRootPath' => 'Resources/Private/Partials',
+                               'partialRootPaths' => NULL,
+                               'layoutRootPath' => 'Resources/Private/Layouts',
+                               'layoutRootPaths' => array('Resources/Layouts', 'Some/Fallback/Path'),
+                               'bubbleControllerAndSubpackage' => TRUE,
+                               'formatIsOptional' => TRUE,
+                               'pattern' => 'Static/@layoutRoot/@subpackage/@controller/@layout.@format',
+                               'expectedResult' => array(
+                                       'Static/Resources/Layouts/Some/Sub/Package/SomeController/@layout.html',
+                                       'Static/Resources/Layouts/Some/Sub/Package/SomeController/@layout',
+                                       'Static/Resources/Layouts/Some/Sub/Package/@layout.html',
+                                       'Static/Resources/Layouts/Some/Sub/Package/@layout',
+                                       'Static/Resources/Layouts/Sub/Package/@layout.html',
+                                       'Static/Resources/Layouts/Sub/Package/@layout',
+                                       'Static/Resources/Layouts/Package/@layout.html',
+                                       'Static/Resources/Layouts/Package/@layout',
+                                       'Static/Resources/Layouts/@layout.html',
+                                       'Static/Resources/Layouts/@layout',
+                                       'Static/Some/Fallback/Path/Some/Sub/Package/SomeController/@layout.html',
+                                       'Static/Some/Fallback/Path/Some/Sub/Package/SomeController/@layout',
+                                       'Static/Some/Fallback/Path/Some/Sub/Package/@layout.html',
+                                       'Static/Some/Fallback/Path/Some/Sub/Package/@layout',
+                                       'Static/Some/Fallback/Path/Sub/Package/@layout.html',
+                                       'Static/Some/Fallback/Path/Sub/Package/@layout',
+                                       'Static/Some/Fallback/Path/Package/@layout.html',
+                                       'Static/Some/Fallback/Path/Package/@layout',
+                                       'Static/Some/Fallback/Path/@layout.html',
+                                       'Static/Some/Fallback/Path/@layout',
+                               )
+                       ),
+                       // combined fallback paths
+                       array(
+                               'package' => 'Some.Package',
+                               'subPackage' => 'Some\\Sub\\Package',
+                               'controller' => 'SomeController',
+                               'format' => 'html',
+                               'templateRootPath' => 'Resources/Private/Templates',
+                               'templateRootPaths' => array('Resources/Templates', 'Templates/Fallback1', 'Templates/Fallback2'),
+                               'partialRootPath' => 'Resources/Private/Partials',
+                               'partialRootPaths' => array('Resources/Partials'),
+                               'layoutRootPath' => 'Resources/Private/Layouts',
+                               'layoutRootPaths' => array('Resources/Layouts', 'Layouts/Fallback1'),
+                               'bubbleControllerAndSubpackage' => FALSE,
+                               'formatIsOptional' => TRUE,
+                               'pattern' => '@layoutRoot/@templateRoot/@partialRoot/@subpackage/@controller/foo',
+                               'expectedResult' => array(
+                                       'Resources/Layouts/Resources/Templates/Resources/Partials/Some/Sub/Package/SomeController/foo',
+                                       'Layouts/Fallback1/Resources/Templates/Resources/Partials/Some/Sub/Package/SomeController/foo',
+                                       'Resources/Layouts/Templates/Fallback1/Resources/Partials/Some/Sub/Package/SomeController/foo',
+                                       'Layouts/Fallback1/Templates/Fallback1/Resources/Partials/Some/Sub/Package/SomeController/foo',
+                                       'Resources/Layouts/Templates/Fallback2/Resources/Partials/Some/Sub/Package/SomeController/foo',
+                                       'Layouts/Fallback1/Templates/Fallback2/Resources/Partials/Some/Sub/Package/SomeController/foo',
+                               )
+                       ),
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider expandGenericPathPatternDataProvider()
+        *
+        * @param string $package
+        * @param string $subPackage
+        * @param string $controller
+        * @param string $format
+        * @param string $templateRootPath
+        * @param array $templateRootPaths
+        * @param string $partialRootPath
+        * @param array $partialRootPaths
+        * @param string $layoutRootPath
+        * @param array $layoutRootPaths
+        * @param boolean $bubbleControllerAndSubpackage
+        * @param boolean $formatIsOptional
+        * @param string $pattern
+        * @param string $expectedResult
+        */
+       public function expandGenericPathPatternTests($package, $subPackage, $controller, $format, $templateRootPath, array $templateRootPaths = NULL, $partialRootPath, array $partialRootPaths = NULL, $layoutRootPath, array $layoutRootPaths = NULL, $bubbleControllerAndSubpackage, $formatIsOptional, $pattern, $expectedResult) {
+               $mockControllerContext = $this->setupMockControllerContextForPathResolving($package, $subPackage, $controller, $format);
+
+               /** @var \TYPO3\CMS\Fluid\View\TemplateView $templateView */
+               $templateView = $this->getAccessibleMock('TYPO3\\CMS\\Fluid\\View\\TemplateView', array('dummy'), array(), '', FALSE);
+               $templateView->setControllerContext($mockControllerContext);
+               if ($templateRootPath !== NULL) {
+                       $templateView->setTemplateRootPath($templateRootPath);
+               }
+               if ($templateRootPaths !== NULL) {
+                       $templateView->setTemplateRootPaths($templateRootPaths);
+               }
+
+               if ($partialRootPath !== NULL) {
+                       $templateView->setPartialRootPath($partialRootPath);
+               }
+               if ($partialRootPaths !== NULL) {
+                       $templateView->setPartialRootPaths($partialRootPaths);
+               }
+
+               if ($layoutRootPath !== NULL) {
+                       $templateView->setLayoutRootPath($layoutRootPath);
+               }
+               if ($layoutRootPaths !== NULL) {
+                       $templateView->setLayoutRootPaths($layoutRootPaths);
+               }
+
+               $actualResult = $templateView->_call('expandGenericPathPattern', $pattern, $bubbleControllerAndSubpackage, $formatIsOptional);
+               $this->assertEquals($expectedResult, $actualResult);
+       }
+
+       /**
         * @test
         */
        public function expandGenericPathPatternWorksWithBubblingDisabledAndFormatNotOptional() {
                $mockControllerContext = $this->setupMockControllerContextForPathResolving('MyPackage', NULL, 'My', 'html');
 
-               $templateView = $this->getAccessibleMock('TYPO3\\CMS\\Fluid\\View\\TemplateView', array('getTemplateRootPath', 'getPartialRootPath', 'getLayoutRootPath'), array(), '', FALSE);
+               $templateView = $this->getAccessibleMock('TYPO3\\CMS\\Fluid\\View\\TemplateView', array('getTemplateRootPaths', 'getPartialRootPath', 'getLayoutRootPath'), array(), '', FALSE);
                $templateView->_set('controllerContext', $mockControllerContext);
-               $templateView->expects($this->any())->method('getTemplateRootPath')->will($this->returnValue('Resources/Private/'));
+               $templateView->expects($this->any())->method('getTemplateRootPaths')->will($this->returnValue(array('Resources/Private/')));
 
                $expected = array('Resources/Private/Templates/My/@action.html');
                $actual = $templateView->_call('expandGenericPathPattern', '@templateRoot/Templates/@subpackage/@controller/@action.@format', FALSE, FALSE);
@@ -70,9 +539,9 @@ class TemplateViewTest extends \TYPO3\CMS\Extbase\Tests\Unit\BaseTestCase {
        public function expandGenericPathPatternWorksWithSubpackageAndBubblingDisabledAndFormatNotOptional() {
                $mockControllerContext = $this->setupMockControllerContextForPathResolving('MyPackage', 'MySubPackage', 'My', 'html');
 
-               $templateView = $this->getAccessibleMock('TYPO3\\CMS\\Fluid\\View\\TemplateView', array('getTemplateRootPath', 'getPartialRootPath', 'getLayoutRootPath'), array(), '', FALSE);
+               $templateView = $this->getAccessibleMock('TYPO3\\CMS\\Fluid\\View\\TemplateView', array('getTemplateRootPaths', 'getPartialRootPath', 'getLayoutRootPath'), array(), '', FALSE);
                $templateView->_set('controllerContext', $mockControllerContext);
-               $templateView->expects($this->any())->method('getTemplateRootPath')->will($this->returnValue('Resources/Private/'));
+               $templateView->expects($this->any())->method('getTemplateRootPaths')->will($this->returnValue(array('Resources/Private/')));
                $actual = $templateView->_call('expandGenericPathPattern', '@templateRoot/Templates/@subpackage/@controller/@action.@format', FALSE, FALSE);
 
                $expected = array(
@@ -87,9 +556,9 @@ class TemplateViewTest extends \TYPO3\CMS\Extbase\Tests\Unit\BaseTestCase {
        public function expandGenericPathPatternWorksWithSubpackageAndBubblingDisabledAndFormatOptional() {
                $mockControllerContext = $this->setupMockControllerContextForPathResolving('MyPackage', 'MySubPackage', 'My', 'html');
 
-               $templateView = $this->getAccessibleMock('TYPO3\\CMS\\Fluid\\View\\TemplateView', array('getTemplateRootPath', 'getPartialRootPath', 'getLayoutRootPath'), array(), '', FALSE);
+               $templateView = $this->getAccessibleMock('TYPO3\\CMS\\Fluid\\View\\TemplateView', array('getTemplateRootPaths', 'getPartialRootPath', 'getLayoutRootPath'), array(), '', FALSE);
                $templateView->_set('controllerContext', $mockControllerContext);
-               $templateView->expects($this->any())->method('getTemplateRootPath')->will($this->returnValue('Resources/Private/'));
+               $templateView->expects($this->any())->method('getTemplateRootPaths')->will($this->returnValue(array('Resources/Private/')));
                $actual = $templateView->_call('expandGenericPathPattern', '@templateRoot/Templates/@subpackage/@controller/@action.@format', FALSE, TRUE);
 
                $expected = array(
@@ -105,9 +574,9 @@ class TemplateViewTest extends \TYPO3\CMS\Extbase\Tests\Unit\BaseTestCase {
        public function expandGenericPathPatternWorksWithSubpackageAndBubblingEnabledAndFormatOptional() {
                $mockControllerContext = $this->setupMockControllerContextForPathResolving('MyPackage', 'MySubPackage', 'My', 'html');
 
-               $templateView = $this->getAccessibleMock('TYPO3\\CMS\\Fluid\\View\\TemplateView', array('getTemplateRootPath', 'getPartialRootPath', 'getLayoutRootPath'), array(), '', FALSE);
+               $templateView = $this->getAccessibleMock('TYPO3\\CMS\\Fluid\\View\\TemplateView', array('getTemplateRootPaths', 'getPartialRootPath', 'getLayoutRootPath'), array(), '', FALSE);
                $templateView->_set('controllerContext', $mockControllerContext);
-               $templateView->expects($this->any())->method('getTemplateRootPath')->will($this->returnValue('Resources/Private/'));
+               $templateView->expects($this->any())->method('getTemplateRootPaths')->will($this->returnValue(array('Resources/Private/')));
                $actual = $templateView->_call('expandGenericPathPattern', '@templateRoot/Templates/@subpackage/@controller/@action.@format', TRUE, TRUE);
 
                $expected = array(
@@ -122,59 +591,47 @@ class TemplateViewTest extends \TYPO3\CMS\Extbase\Tests\Unit\BaseTestCase {
        }
 
        /**
-        * Helper to build mock controller context needed to test expandGenericPathPattern.
-        *
-        * @param string $packageKey
-        * @param string $subPackageKey
-        * @param string $controllerClassName
-        * @param string $format
-        * @return \PHPUnit_Framework_MockObject_MockObject
+        * @test
         */
-       protected function setupMockControllerContextForPathResolving($packageKey, $subPackageKey, $controllerName, $format) {
-               $controllerObjectName = "TYPO3\\$packageKey\\" . ($subPackageKey != $subPackageKey . '\\' ? : '') . 'Controller\\' . $controllerName . 'Controller';
-               $mockRequest = $this->getMock('TYPO3\\CMS\\Extbase\\Mvc\\Web\\Request');
-               $mockRequest->expects($this->any())->method('getControllerPackageKey')->will($this->returnValue($packageKey));
-               $mockRequest->expects($this->any())->method('getControllerSubPackageKey')->will($this->returnValue($subPackageKey));
-               $mockRequest->expects($this->any())->method('getControllerName')->will($this->returnValue($controllerName));
-               $mockRequest->expects($this->any())->method('getControllerObjectName')->will($this->returnValue($controllerObjectName));
-               $mockRequest->expects($this->any())->method('getFormat')->will($this->returnValue($format));
-
-               $mockControllerContext = $this->getMock('TYPO3\\CMS\\Extbase\\Mvc\\Controller\\ControllerContext', array('getRequest'), array(), '', FALSE);
-               $mockControllerContext->expects($this->any())->method('getRequest')->will($this->returnValue($mockRequest));
-
-               return $mockControllerContext;
+       public function getTemplateRootPathsReturnsUserSpecifiedTemplatePaths() {
+               $templateView = $this->getAccessibleMock('TYPO3\\CMS\\Fluid\\View\\TemplateView', array('dummy'), array(), '', FALSE);
+               $templateView->setTemplateRootPath('/foo/bar');
+               $expected = array('/foo/bar');
+               $actual = $templateView->_call('getTemplateRootPaths');
+               $this->assertEquals($expected, $actual, 'A set template root path was not returned correctly.');
        }
 
        /**
         * @test
         */
-       public function getTemplateRootPathReturnsUserSpecifiedTemplatePath() {
+       public function setTemplateRootPathOverrulesSetTemplateRootPaths() {
                $templateView = $this->getAccessibleMock('TYPO3\\CMS\\Fluid\\View\\TemplateView', array('dummy'), array(), '', FALSE);
                $templateView->setTemplateRootPath('/foo/bar');
-               $expected = '/foo/bar';
-               $actual = $templateView->_call('getTemplateRootPath');
+               $templateView->setTemplateRootPaths(array('/overruled/path'));
+               $expected = array('/overruled/path');
+               $actual = $templateView->_call('getTemplateRootPaths');
                $this->assertEquals($expected, $actual, 'A set template root path was not returned correctly.');
        }
 
        /**
         * @test
         */
-       public function getPartialRootPathReturnsUserSpecifiedPartialPath() {
+       public function getPartialRootPathsReturnsUserSpecifiedPartialPath() {
                $templateView = $this->getAccessibleMock('TYPO3\\CMS\\Fluid\\View\\TemplateView', array('dummy'), array(), '', FALSE);
                $templateView->setPartialRootPath('/foo/bar');
-               $expected = '/foo/bar';
-               $actual = $templateView->_call('getPartialRootPath');
+               $expected = array('/foo/bar');
+               $actual = $templateView->_call('getPartialRootPaths');
                $this->assertEquals($expected, $actual, 'A set partial root path was not returned correctly.');
        }
 
        /**
         * @test
         */
-       public function getLayoutRootPathReturnsUserSpecifiedPartialPath() {
+       public function getLayoutRootPathsReturnsUserSpecifiedPartialPath() {
                $templateView = $this->getAccessibleMock('TYPO3\\CMS\\Fluid\\View\\TemplateView', array('dummy'), array(), '', FALSE);
                $templateView->setLayoutRootPath('/foo/bar');
-               $expected = '/foo/bar';
-               $actual = $templateView->_call('getLayoutRootPath');
+               $expected = array('/foo/bar');
+               $actual = $templateView->_call('getLayoutRootPaths');
                $this->assertEquals($expected, $actual, 'A set partial root path was not returned correctly.');
        }
 
@@ -182,15 +639,29 @@ class TemplateViewTest extends \TYPO3\CMS\Extbase\Tests\Unit\BaseTestCase {
         * @test
         */
        public function pathToPartialIsResolvedCorrectly() {
-               $this->markTestSkipped('Needs to be finished');
                vfsStreamWrapper::register();
-               mkdir('vfs://MyTemplates');
-               \file_put_contents('vfs://MyTemplates/MyCoolAction.html', 'contentsOfMyCoolAction');
-               $mockRootDirectory = vfsStreamDirectory::create('ExamplePackagePath/Resources/Private/Partials');
-               $mockRootDirectory->getChild('Resources/Private/Partials')->addChild('Partials');
-               vfsStreamWrapper::setRoot($mockRootDirectory);
+               mkdir('vfs://MyPartials');
+               \file_put_contents('vfs://MyPartials/SomePartial', 'contentsOfSomePartial');
 
-               $this->getAccessibleMock('TYPO3\\CMS\\Fluid\\Core\\Parser\\TemplateParser', array(''), array(), '', FALSE);
+               $paths = array(
+                       'vfs://NonExistantDir/UnknowFile.html',
+                       'vfs://MyPartials/SomePartial.html',
+                       'vfs://MyPartials/SomePartial'
+               );
+
+               $templateView = $this->getAccessibleMock('TYPO3\\CMS\\Fluid\\View\\TemplateView', array('expandGenericPathPattern', 'resolveFileNamePath'), array(), '', FALSE);
+               $templateView->expects($this->once())->method('expandGenericPathPattern')->with('@partialRoot/@subpackage/@partial.@format', TRUE, TRUE)->will($this->returnValue($paths));
+               $templateView->expects($this->any())->method('resolveFileNamePath')->will($this->onConsecutiveCalls(
+                       $paths[0],
+                       $paths[1],
+                       $paths[2]
+               ));
+
+               $templateView->setTemplateRootPath('MyTemplates');
+               $templateView->setPartialRootPath('MyPartials');
+               $templateView->setLayoutRootPath('MyLayouts');
+
+               $this->assertSame('contentsOfSomePartial', $templateView->_call('getPartialSource', 'SomePartial'));
        }
 
        /**
@@ -202,12 +673,16 @@ class TemplateViewTest extends \TYPO3\CMS\Extbase\Tests\Unit\BaseTestCase {
                \file_put_contents('vfs://MyTemplates/MyCoolAction.html', 'contentsOfMyCoolAction');
 
                $paths = array(
-                       'vfs://NonExistantDir/UnknowFile.html',
+                       'vfs://NonExistantDir/UnknownFile.html',
                        'vfs://MyTemplates/@action.html'
                );
 
-               $templateView = $this->getAccessibleMock('TYPO3\\CMS\\Fluid\\View\\TemplateView', array('expandGenericPathPattern'), array(), '', FALSE);
+               $templateView = $this->getAccessibleMock('TYPO3\\CMS\\Fluid\\View\\TemplateView', array('expandGenericPathPattern', 'resolveFileNamePath'), array(), '', FALSE);
                $templateView->expects($this->once())->method('expandGenericPathPattern')->with('@templateRoot/@subpackage/@controller/@action.@format', FALSE, FALSE)->will($this->returnValue($paths));
+               $templateView->expects($this->any())->method('resolveFileNamePath')->will($this->onConsecutiveCalls(
+                       $paths[0],
+                       'vfs://MyTemplates/MyCoolAction.html'
+               ));
 
                $templateView->setTemplateRootPath('MyTemplates');
                $templateView->setPartialRootPath('MyPartials');