[
  {
    "path": ".composer-auth.json",
    "content": "{\n    \"github-oauth\": {\n        \"github.com\": \"PLEASE DO NOT USE THIS TOKEN IN YOUR OWN PROJECTS/FORKS\",\n        \"github.com\": \"This token is reserved for testing the puli/* repositories\",\n        \"github.com\": \"a9debbffdd953ee9b3b82dbc3b807cde2086bb86\"\n    }\n}\n"
  },
  {
    "path": ".gitattributes",
    "content": "# Path-based git attributes\n# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html\n\n# Ignore all test and documentation with \"export-ignore\".\n/.composer-auth.json  export-ignore\n/.gitattributes       export-ignore\n/.gitignore           export-ignore\n/.styleci.yml         export-ignore\n/.travis.yml          export-ignore\n/appveyor.yml         export-ignore\n/phpunit.xml.dist     export-ignore\n/tests                export-ignore\n"
  },
  {
    "path": ".gitignore",
    "content": "/vendor/\ncomposer.lock\n"
  },
  {
    "path": ".styleci.yml",
    "content": "preset: symfony\n\nenabled:\n    - ordered_use\n    - strict\n\ndisabled:\n    - empty_return\n    - phpdoc_annotation_without_dot # This is still buggy: https://github.com/symfony/symfony/pull/19198\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: php\n\nsudo: false\n\ncache:\n  directories:\n    - $HOME/.composer/cache/files\n\nmatrix:\n  include:\n    - php: 5.3\n    - php: 5.4\n    - php: 5.5\n    - php: 5.6\n    - php: hhvm\n    - php: nightly\n    - php: 7.0\n      env: COVERAGE=yes\n    - php: 7.0\n      env: COMPOSER_FLAGS='--prefer-lowest --prefer-stable'\n  allow_failures:\n    - php: hhvm\n    - php: nightly\n  fast_finish: true\n\nbefore_install:\n  - if [[ $TRAVIS_PHP_VERSION != hhvm && $COVERAGE != yes ]]; then phpenv config-rm xdebug.ini; fi;\n  - if [[ $TRAVIS_REPO_SLUG = puli/repository ]]; then cp .composer-auth.json ~/.composer/auth.json; fi;\n  - composer self-update\n\ninstall: composer update $COMPOSER_FLAGS --prefer-dist --no-interaction\n\nscript: if [[ $COVERAGE = yes ]]; then vendor/bin/phpunit --verbose --coverage-clover=coverage.clover; else vendor/bin/phpunit --verbose; fi\n\nafter_script: if [[ $COVERAGE = yes ]]; then wget https://scrutinizer-ci.com/ocular.phar && php ocular.phar code-coverage:upload --format=php-clover coverage.clover; fi\n\nnotifications:\n  webhooks:\n    urls: ['https://webhooks.gitter.im/e/9ccc2378e6c0de6480f8']\n    on_success: change\n    on_failure: always\n    on_start:   never\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "Changelog\n=========\n\n* 1.0.0-beta11 (@release_date@)\n\n * added `ResourceBinding`\n * added `ResourceBindingInitializer`\n\n* 1.0.0-beta10 (2016-02-05)\n\n * fixed regression in `FilesystemRepository::clear()` which caused files in\n   symlinked directories to be deleted\n\n* 1.0.0-beta9 (2016-01-14)\n\n * made compatible with Symfony 3.0\n * added JSON schema for path mapping files: `path-mappings-schema-1.0.json`\n * upgraded to webmozart/glob 4.1 to improve performance\n * renamed `Resource` to `PuliResource`\n * renamed `AbstractPathMappingRepository` to `AbstractJsonRepository`\n * renamed `PathMappingRepository` to `JsonRepository`\n * renamed `OptimizedPathMappingRepository` to `OptimizedJsonRepository`\n * changed constructor arguments of JSON repositories from `KeyValueStore`\n   to paths of JSON files\n * added `AbstractEditableRepository`\n * added `ChangeStream`\n * added `VersionList`\n * added `NoVersionFoundException`\n * added `InMemoryChangeStream`\n * added `KeyValueStoreChangeStream`\n * added `JsonChangeStream`\n * added `PuliResource::getVersions()`\n * added `ResourceRepository::getVersions()`\n * added `LinkResource::getTarget()`\n * made `LinkResource` serializable\n\n* 1.0.0-beta8 (2015-10-05)\n\n * fixed problem with slash handling in `PathMappingRepository`\n * added `LinkResource`\n\n* 1.0.0-beta7 (2015-08-24)\n\n * improved Windows compatibility\n * fixed minimum package versions in composer.json\n * switched to webmozart/glob 3.1 to fix Windows issues\n * fixed resource overriding in the `PathMappingRepository`\n * supported removal of path mappings in `PathMappingRepository`\n\n* 1.0.0-beta6 (2015-08-12)\n\n * added `PathMappingRepository`\n * added `OptimizedPathMappingRepository`\n * fixed repository building on Windows\n * upgraded to webmozart/glob 3.0 for enhanced performance of file iteration\n * added `AbstractRepository` and `AbstractPathMappingRepository`\n * fixed reading of file modification time for symlinks\n\n* 1.0.0-beta5 (2015-05-29)\n\n * upgraded to webmozart/path-util 2.0\n * fixed overriding of files in deep directories\n\n* 1.0.0-beta4 (2015-04-13)\n\n * removed `Resource::getPayload()`\n * removed `$code` arguments from static exception factory methods\n * upgraded to webmozart/glob 2.0\n\n* 1.0.0-beta3 (2015-03-19)\n\n * added `Resource::getPayload()`\n * removed `DetachedException`\n * replaced `Assert` by webmozart/assert\n * added support for relative symlinks to `FilesystemRepository`\n * `FilesystemRepository` now falls back to copies if symlinks are not supported\n\n* 1.0.0-beta2 (2015-01-27)\n\n * added `NullRepository`\n * removed dependency to beberlei/assert\n * symfony/filesystem is now an optional dependency that is only needed when\n   using the FilesystemRepository\n\n* 1.0.0-beta (2015-01-12)\n\n * renamed `Selector` to `Glob` and moved it to package \"webmozart/glob\"\n * removed `AttachableResourceInterface`\n * removed `DirectoryResourceInterface`\n * removed `FileResourceInterface`\n * removed `OverriddenPathLoaderInterface`\n * removed `Interface` suffix of all interfaces\n * `ResourceRepository::find()` now matches directory separators \"/\" when given\n   a wildcard \"*\"\n * merged `AbstractResource` and `DirectoryResource` into `GenericResource`\n * renamed `LocalDirectoryResource` to `DirectoryResource`\n * renamed `LocalFileResource` to `FileResource`\n * removed `LocalResource::getAllLocalPaths`\n * rename `LocalResource::getLocalPath` to `LocalResource::getFilesystemPath`\n * renamed `LocalResource` to `FilesystemResource`\n * renamed `LocalResourceCollection` to `FilesystemResourceCollection`\n * removed `createAttached()` from `GenericResource`, `FileResource` and\n   `DirectoryResource`\n * removed tagging\n * renamed`ResourceRepository` to `InMemoryRepository`\n * renamed `ResourceCollection` to `ArrayResourceCollection`\n * renamed `RecursiveResourceIterator` to `RecursiveResourceIteratorIterator`\n * renamed `ManageableResourceRepository` to `EditableRepository`\n * removed `UriRepository`\n * added `$scheme` argument to `ResourceStreamWrapper::register()` and\n   `ResourceStreamWrapper::unregister()`\n * added `ResourceNotFoundException::forPath()`\n * added `NoDirectoryException::forPath()`\n * moved contents of `Puli\\Repository\\Filesystem\\Iterator` to `Puli\\Repository\\Iterator`\n * moved contents of `Puli\\Repository\\Filesystem\\Resource` to `Puli\\Repository\\Resource`\n * moved `FilesystemRepository` to `Puli\\Repository`\n * removed `PhpCacheRepository`\n * added domain-specific `Assert` class\n * moved API interfaces to `Api` sub-namespace\n * removed notions of \"directories\" and \"files\". All resources can have children\n   and a body now.\n * added `ResourceRepository::listChildren()` and `hasChildren()`\n * added `ResourceMetadata` and `FilesystemMetadata`\n * added methods to `Resource`:\n   * `getChild()`\n   * `hasChild()`\n   * `hasChildren()`\n   * `listChildren()`\n   * `getMetadata()`\n   * `getRepository()`\n   * `getRepositoryPath()`\n   * `attachTo()`\n   * `detach()`\n   * `isAttached()`\n   * `createReference()`\n   * `isReference()`\n * made `Resource` extend `Serializable`\n * added `EditableRepository::clear()`\n * removed backend repositories from `InMemoryRepository` and `FilesystemRepository`\n * added symlink support to `FilesystemRepository`\n * removed `FilesystemException`\n * removed `InvalidPathException`\n * removed `UnsupportedSchemeException`\n * replaced `NoDirectoryException` by `UnsupportedOperationException`\n * removed `CompositeRepository` from the 1.0 branch\n\n* 1.0.0-alpha4 (2014-12-03)\n\n * moved extensions to separate repositories in https://github.com/puli\n * moved documentation to separate repository: https://github.com/puli/docs\n * moved `Path` to \"webmozart/path-util\" package\n * moved all code to `Puli\\Repository` namespace\n * rearranged the directory structure\n * added `ResourceCollectionIterator`\n * added `ResourceIteratorInterface`\n * added `RecursiveResourceIterator`\n * added `RecursiveResourceIteratorInterface`\n * added `ResourceFilterIterator`\n * renamed `ResourceRepositoryInterface` to `ManageableRepositoryInterface`\n * renamed `ResourceLocatorInterface` to `ResourceRepositoryInterface`\n * renamed all \"locators\" to \"repositories\"\n * moved all filesystem specific code to `Filesystem` namespace\n * made `ResourceInterface` independent of the filesystem. The filesystem\n   specific methods are now in `LocalResourceInterface`\n * `getAlternativePaths()` is now called `getAllLocalPaths()`\n * added `getContents()`, `getSize()`, `getLastAccessedAt()` and\n   `getLastModifiedAt()` to `FileResourceInterface`\n * removed all pattern-related classes. This logic is now provided by the\n   `Selector` class\n * `ResourceRepository::remove()`, `tag()` and `untag()` now return the number\n   of affected resources\n * added `UriRepository::getDefaultScheme()` and `setDefaultScheme()`\n * renamed `getByTag()` to `findByTag()`\n * added `merge()` to `ResourceCollectionInterface`\n * added `CompositeRepository`\n * removed `LazyDirectoryResource`\n * fixed ResourceRepository::add() to be deterministic when selectors are passed. Closes #17\n\n* 1.0.0-alpha3 (2014-02-22)\n\n * renamed `PhpResourceLocator` to `PhpCacheLocator`\n * renamed `PhpResourceLocatorDumper` to `PhpCacheDumper`\n * added `FilesystemLocator`\n * removed `ResourceDiscoveringInterface`\n * a base `ResourceLocatorInterface` can now be passed to `ResourceRepository`\n * instead of arrays, `ResourceCollection` objects are now returned everywhere\n * renamed `ResourceInterface::getPath()` to `getRealPath()`\n * renamed `ResourceInterface::getRepositoryPath()` to `getPath()`\n * added an extension for the templating engine Twig\n * added an extension for the Symfony Config and HttpKernel components\n\n* 1.0.0-alpha2 (2014-02-14)\n\n * fixed \"Maximum function nesting level\" error on Windows\n * pushed minimum PHP version to 5.3.9\n * removed `TagInterface` and descending classes\n * added support for dot segments (\".\" and \"..\")\n * removed `CreationNotAllowedException`\n * removed `RemoveNotAllowedException`\n * removed `RenameNotAllowedException`\n * added `UnsupportedOperationException`\n * added `Path`\n * added `Uri`\n * added `UriLocatorInterface` and `UriLocator`\n * changed `ResourceStreamWrapper::register()` to take a `UriLocatorInterface`\n   instance instead of a scheme and a resource locator\n\n* 1.0.0-alpha1 (2014-02-04)\n\n * first alpha release\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2014 Bernhard Schussek\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "The Puli Repository Component\n=============================\n\n[![Build Status](https://travis-ci.org/puli/repository.svg?branch=1.0)](https://travis-ci.org/puli/repository)\n[![Build status](https://ci.appveyor.com/api/projects/status/a0g5jdtj78wv53c0/branch/1.0?svg=true)](https://ci.appveyor.com/project/webmozart/repository/branch/1.0)\n[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/puli/repository/badges/quality-score.png?b=1.0)](https://scrutinizer-ci.com/g/puli/repository/?branch=1.0)\n[![Latest Stable Version](https://poser.pugx.org/puli/repository/v/stable.svg)](https://packagist.org/packages/puli/repository)\n[![Total Downloads](https://poser.pugx.org/puli/repository/downloads.svg)](https://packagist.org/packages/puli/repository)\n[![Dependency Status](https://www.versioneye.com/php/puli:repository/1.0.0/badge.svg)](https://www.versioneye.com/php/puli:repository/1.0.0)\n\nLatest release: [1.0.0-beta10](https://packagist.org/packages/puli/repository#1.0.0-beta10)\n\nPHP >= 5.3.9\n\nThe [Puli] Repository Component provides an API for storing arbitrary resources\nin a filesystem-like repository:\n\n```php\nuse Puli\\Repository\\InMemoryRepository;\nuse Puli\\Repository\\Resource\\DirectoryResource;\n\n$repo = new InMemoryRepository();\n$repo->add('/config', new DirectoryResource('/path/to/resources/config'));\n\n// /path/to/resources/config/routing.yml\necho $repo->get('/config/routing.yml')->getBody();\n```\n\nThe following [`ResourceRepository`] implementations are currently supported:\n\n* [`InMemoryRepository`]\n* [`FilesystemRepository`]\n* [`NullRepository`]\n* [`JsonRepository`]\n* [`OptimizedJsonRepository`]\n\nThe following [`Resource`] implementations are currently supported:\n\n* [`GenericResource`]\n* [`FileResource`]\n* [`DirectoryResource`]\n* [`LinkResource`]\n\nAuthors\n-------\n\n* [Bernhard Schussek] a.k.a. [@webmozart]\n* [The Community Contributors]\n\nInstallation\n------------\n\nFollow the [Getting Started] guide to install Puli in your project.\n\nDocumentation\n-------------\n\nRead the [Puli Documentation] to learn more about Puli.\n\nContribute\n----------\n\nContributions to Puli are always welcome!\n\n* Report any bugs or issues you find on the [issue tracker].\n* You can grab the source code at Puli’s [Git repository].\n\nSupport\n-------\n\nIf you are having problems, send a mail to bschussek@gmail.com or shout out to\n[@webmozart] on Twitter.\n\nLicense\n-------\n\nAll contents of this package are licensed under the [MIT license].\n\n[Puli]: http://puli.io\n[Bernhard Schussek]: http://webmozarts.com\n[The Community Contributors]: https://github.com/puli/repository/graphs/contributors\n[Installation guide]: http://docs.puli.io/en/latest/installation.html\n[Puli Documentation]: http://docs.puli.io/en/latest/index.html\n[issue tracker]: https://github.com/puli/issues/issues\n[Git repository]: https://github.com/puli/repository\n[@webmozart]: https://twitter.com/webmozart\n[MIT license]: LICENSE\n[`ResourceRepository`]: http://api.puli.io/latest/class-Puli.Repository.Api.ResourceRepository.html\n[`InMemoryRepository`]: http://api.puli.io/latest/class-Puli.Repository.InMemoryRepository.html\n[`FilesystemRepository`]: http://api.puli.io/latest/class-Puli.Repository.FilesystemRepository.html\n[`NullRepository`]: http://api.puli.io/latest/class-Puli.Repository.NullRepository.html\n[`JsonRepository`]: http://api.puli.io/latest/class-Puli.Repository.JsonRepository.html\n[`OptimizedJsonRepository`]: http://api.puli.io/latest/class-Puli.Repository.OptimizedJsonRepository.html\n[`Resource`]: http://api.puli.io/latest/class-Puli.Repository.Api.Resource.Resource.html\n[`GenericResource`]: http://api.puli.io/latest/class-Puli.Repository.Resource.GenericResource.html\n[`FileResource`]: http://api.puli.io/latest/class-Puli.Repository.Resource.FileResource.html\n[`DirectoryResource`]: http://api.puli.io/latest/class-Puli.Repository.Resource.DirectoryResource.html\n[`LinkResource`]: http://api.puli.io/latest/class-Puli.Repository.Resource.LinkResource.html\n"
  },
  {
    "path": "appveyor.yml",
    "content": "build: false\nplatform: x86\nclone_folder: c:\\projects\\puli\\repository\n\ncache:\n  - c:\\php -> appveyor.yml\n\ninit:\n  - SET PATH=c:\\php;%PATH%\n  - SET COMPOSER_NO_INTERACTION=1\n  - SET PHP=1\n\ninstall:\n  - IF EXIST c:\\php (SET PHP=0) ELSE (mkdir c:\\php)\n  - cd c:\\php\n  - IF %PHP%==1 appveyor DownloadFile http://windows.php.net/downloads/releases/archives/php-7.0.0-nts-Win32-VC14-x86.zip\n  - IF %PHP%==1 7z x php-7.0.0-nts-Win32-VC14-x86.zip -y >nul\n  - IF %PHP%==1 del /Q *.zip\n  - IF %PHP%==1 echo @php %%~dp0composer.phar %%* > composer.bat\n  - IF %PHP%==1 copy /Y php.ini-development php.ini\n  - IF %PHP%==1 echo max_execution_time=1200 >> php.ini\n  - IF %PHP%==1 echo date.timezone=\"UTC\" >> php.ini\n  - IF %PHP%==1 echo extension_dir=ext >> php.ini\n  - IF %PHP%==1 echo extension=php_openssl.dll >> php.ini\n  - IF %PHP%==1 echo extension=php_mbstring.dll >> php.ini\n  - IF %PHP%==1 echo extension=php_fileinfo.dll >> php.ini\n  - appveyor DownloadFile https://getcomposer.org/composer.phar\n  - cd c:\\projects\\puli\\repository\n  - mkdir %APPDATA%\\Composer\n  - IF %APPVEYOR_REPO_NAME%==puli/repository copy /Y .composer-auth.json %APPDATA%\\Composer\\auth.json\n  - composer update --prefer-dist --no-progress --ansi\n\ntest_script:\n  - cd c:\\projects\\puli\\repository\n  - vendor\\bin\\phpunit.bat --verbose\n"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"puli/repository\",\n    \"description\": \"A filesystem-like repository for storing arbitrary resources.\",\n    \"homepage\": \"http://puli.io\",\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"Bernhard Schussek\",\n            \"email\": \"bschussek@gmail.com\"\n        }\n    ],\n    \"require\": {\n        \"php\": \"^5.3.9|^7.0\",\n        \"webmozart/path-util\": \"^2.2\",\n        \"webmozart/glob\": \"^4.1\",\n        \"webmozart/assert\": \"^1.0\",\n        \"psr/log\": \"^1.0\"\n    },\n    \"require-dev\": {\n        \"puli/discovery\": \"^1.0-beta10@dev\",\n        \"webmozart/json\": \"^1.2.1\",\n        \"webmozart/key-value-store\": \"^1.0-beta7\",\n        \"symfony/filesystem\": \"^2.0|^3.0\",\n        \"phpunit/phpunit\": \"^4.6\",\n        \"sebastian/version\": \"^1.0.1\"\n    },\n    \"suggest\": {\n        \"puli/discovery\": \"to provide and discover resources across modules\",\n        \"webmozart/json\": \"to use the JSON repositories\",\n        \"webmozart/key-value-store\": \"to use the key-value store change stream\",\n        \"symfony/filesystem\": \"to use the filesystem repository\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"Puli\\\\Repository\\\\\": \"src/\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Puli\\\\Repository\\\\Tests\\\\\": \"tests/\"\n        }\n    },\n    \"extra\": {\n        \"branch-alias\": {\n            \"dev-master\": \"1.0-dev\"\n        }\n    },\n    \"support\": {\n        \"issues\": \"https://github.com/puli/issues/issues\"\n    }\n}\n"
  },
  {
    "path": "phpunit.xml.dist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<phpunit bootstrap=\"vendor/autoload.php\" colors=\"true\">\n    <testsuites>\n        <testsuite name=\"Puli Test Suite\">\n            <directory suffix=\"Test.php\">./tests/</directory>\n        </testsuite>\n    </testsuites>\n\n    <!-- Whitelist for code coverage -->\n    <filter>\n        <whitelist>\n            <directory suffix=\".php\">./src/</directory>\n        </whitelist>\n    </filter>\n</phpunit>\n"
  },
  {
    "path": "res/schema/path-mappings-schema-1.0.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n    \"type\": \"object\",\n    \"description\": \"A list of mappings from Puli paths to filesystem paths or other Puli paths.\",\n    \"definitions\": {\n        \"relativePath\": {\n            \"description\": \"A filesystem path relative to the base directory of the repository.\",\n            \"type\": \"string\",\n            \"pattern\": \"^([a-zA-Z][^:]|[^/@a-zA-Z])[^/]*(/[^/]+)*$\"\n        },\n        \"absolutePath\": {\n            \"description\": \"An absolute filesystem path.\",\n            \"type\": \"string\",\n            \"pattern\": \"^([a-zA-Z]:)?/(([^/]+/)*[^/]+)?$\"\n        },\n        \"virtualDirectory\": {\n            \"description\": \"A directory in the Puli repository that does not correspond to a real filesystem directory.\",\n            \"type\": \"null\"\n        },\n        \"repositoryLink\": {\n            \"description\": \"A link to another resource in the Puli repository.\",\n            \"type\": \"string\",\n            \"pattern\": \"^@/(([^/]+/)*[^/]+)?$\"\n        },\n        \"singleReference\": {\n            \"description\": \"A reference to a single file, directory, link or virtual directory.\",\n            \"oneOf\": [\n                { \"$ref\": \"#/definitions/relativePath\" },\n                { \"$ref\": \"#/definitions/absolutePath\" },\n                { \"$ref\": \"#/definitions/repositoryLink\" },\n                { \"$ref\": \"#/definitions/virtualDirectory\" }\n            ]\n        },\n        \"combinedReference\": {\n            \"description\": \"A list of references. The first reference has highest priority. Directory contents are merged.\",\n            \"type\": \"array\",\n            \"minItems\": 2,\n            \"uniqueItems\": true,\n            \"items\": { \"$ref\": \"#/definitions/singleReference\" }\n        }\n    },\n    \"patternProperties\": {\n        \"^/(([^/]+/)*[^/]+)?$\": {\n            \"description\": \"A mapping from a Puli path to one or more path references.\",\n            \"oneOf\": [\n                { \"$ref\": \"#/definitions/singleReference\" },\n                { \"$ref\": \"#/definitions/combinedReference\" }\n            ]\n        }\n    },\n    \"properties\": {\n        \"_order\": {\n            \"description\": \"Overrides the default resolution order. By default, mappings for longer paths (e.g. /a/b) have precedence over mapping for shorter paths (e.g. /a). This can be overridden here.\",\n            \"type\": \"object\",\n            \"patternProperties\": {\n                \"^/(([^/]+/)*[^/]+)?$\": {\n                    \"description\": \"Overrides the resolution order for a mapped Puli path. The repository will check the path mappings referred to by the entries of the array in the given order.\",\n                    \"type\": \"array\",\n                    \"minItems\": 2,\n                    \"items\": {\n                        \"description\": \"Contains the path of a path mapping and the number of references to check of that path mapping.\",\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"path\": {\n                                \"description\": \"The Puli path of a mapping in this file.\",\n                                \"type\": \"string\",\n                                \"pattern\": \"^/(([^/]+/)*[^/]+)?$\"\n                            },\n                            \"references\": {\n                                \"description\": \"The number of references of the mapping to check.\",\n                                \"type\": \"integer\",\n                                \"min\": 1\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    },\n    \"additionalProperties\": false\n}\n"
  },
  {
    "path": "src/AbstractEditableRepository.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository;\n\nuse Puli\\Repository\\Api\\ChangeStream\\ChangeStream;\nuse Puli\\Repository\\Api\\EditableRepository;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\n\n/**\n * Abstract base for editable repositories providing tools to avoid code duplication.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n * @author Titouan Galopin <galopintitouan@gmail.com>\n */\nabstract class AbstractEditableRepository extends AbstractRepository implements EditableRepository\n{\n    /**\n     * @var ChangeStream\n     */\n    private $changeStream;\n\n    /**\n     * Create the repository.\n     *\n     * @param ChangeStream|null $changeStream If provided, the repository will log\n     *                                        resources changes in this change stream.\n     */\n    public function __construct(ChangeStream $changeStream = null)\n    {\n        $this->changeStream = $changeStream;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getVersions($path)\n    {\n        if (null === $this->changeStream) {\n            return parent::getVersions($path);\n        }\n\n        return $this->changeStream->getVersions($path, $this);\n    }\n\n    /**\n     * Stores a version of a resource in the change stream.\n     *\n     * @param PuliResource $resource The resource version.\n     */\n    protected function storeVersion(PuliResource $resource)\n    {\n        if (null !== $this->changeStream) {\n            $this->changeStream->append($resource);\n        }\n    }\n\n    /**\n     * Removes all versions of a resource from the change stream.\n     *\n     * @param string $path The Puli path.\n     */\n    protected function removeVersions($path)\n    {\n        if (null !== $this->changeStream) {\n            $this->changeStream->purge($path);\n        }\n    }\n\n    /**\n     * Clears the change stream.\n     */\n    protected function clearVersions()\n    {\n        if (null !== $this->changeStream) {\n            $this->changeStream->clear();\n        }\n    }\n}\n"
  },
  {
    "path": "src/AbstractJsonRepository.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository;\n\nuse Psr\\Log\\LoggerAwareInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Psr\\Log\\LogLevel;\nuse Puli\\Repository\\Api\\ChangeStream\\ChangeStream;\nuse Puli\\Repository\\Api\\Resource\\FilesystemResource;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\nuse Puli\\Repository\\Api\\ResourceCollection;\nuse Puli\\Repository\\Api\\ResourceNotFoundException;\nuse Puli\\Repository\\Api\\UnsupportedResourceException;\nuse Puli\\Repository\\Resource\\Collection\\ArrayResourceCollection;\nuse Puli\\Repository\\Resource\\DirectoryResource;\nuse Puli\\Repository\\Resource\\FileResource;\nuse Puli\\Repository\\Resource\\GenericResource;\nuse Puli\\Repository\\Resource\\LinkResource;\nuse RuntimeException;\nuse Webmozart\\Assert\\Assert;\nuse Webmozart\\Json\\JsonDecoder;\nuse Webmozart\\Json\\JsonEncoder;\nuse Webmozart\\PathUtil\\Path;\n\n/**\n * Base class for repositories backed by a JSON file.\n *\n * The generated JSON file is described by res/schema/repository-schema-1.0.json.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n * @author Titouan Galopin <galopintitouan@gmail.com>\n */\nabstract class AbstractJsonRepository extends AbstractEditableRepository implements LoggerAwareInterface\n{\n    /**\n     * Flag: Whether to stop after the first result.\n     *\n     * @internal\n     */\n    const STOP_ON_FIRST = 1;\n\n    /**\n     * @var array\n     */\n    protected $json;\n\n    /**\n     * @var string\n     */\n    protected $baseDirectory;\n\n    /**\n     * @var string\n     */\n    private $path;\n\n    /**\n     * @var string\n     */\n    private $schemaPath;\n\n    /**\n     * @var JsonEncoder\n     */\n    private $encoder;\n\n    /**\n     * @var LoggerInterface\n     */\n    private $logger;\n\n    /**\n     * Creates a new repository.\n     *\n     * @param string            $path          The path to the JSON file. If\n     *                                         relative, it must be relative to\n     *                                         the base directory.\n     * @param string            $baseDirectory The base directory of the store.\n     *                                         Paths inside that directory are\n     *                                         stored as relative paths. Paths\n     *                                         outside that directory are stored\n     *                                         as absolute paths.\n     * @param bool              $validateJson  Whether to validate the JSON file\n     *                                         against the schema. Slow but\n     *                                         spots problems.\n     * @param ChangeStream|null $changeStream  If provided, the repository will\n     *                                         append resource changes to this\n     *                                         change stream.\n     */\n    public function __construct($path, $baseDirectory, $validateJson = false, ChangeStream $changeStream = null)\n    {\n        parent::__construct($changeStream);\n\n        $this->baseDirectory = $baseDirectory;\n        $this->path = Path::makeAbsolute($path, $baseDirectory);\n        $this->encoder = new JsonEncoder();\n        $this->encoder->setPrettyPrinting(true);\n        $this->encoder->setEscapeSlash(false);\n\n        if ($validateJson) {\n            $this->schemaPath = Path::canonicalize(__DIR__.'/../res/schema/path-mappings-schema-1.0.json');\n        }\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function setLogger(LoggerInterface $logger = null)\n    {\n        $this->logger = $logger;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function add($path, $resource)\n    {\n        if (null === $this->json) {\n            $this->load();\n        }\n\n        $path = $this->sanitizePath($path);\n\n        if ($resource instanceof ResourceCollection) {\n            $this->ensureDirectoryExists($path);\n\n            foreach ($resource as $child) {\n                $this->addResource($path.'/'.$child->getName(), $child);\n            }\n\n            $this->flush();\n\n            return;\n        }\n\n        $this->ensureDirectoryExists(Path::getDirectory($path));\n\n        $this->addResource($path, $resource);\n\n        $this->flush();\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function get($path)\n    {\n        if (null === $this->json) {\n            $this->load();\n        }\n\n        $path = $this->sanitizePath($path);\n        $references = $this->getReferencesForPath($path);\n\n        // Might be null, don't use isset()\n        if (array_key_exists($path, $references)) {\n            return $this->createResource($path, $references[$path]);\n        }\n\n        throw ResourceNotFoundException::forPath($path);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function find($query, $language = 'glob')\n    {\n        if (null === $this->json) {\n            $this->load();\n        }\n\n        $this->failUnlessGlob($language);\n        $query = $this->sanitizePath($query);\n        $results = $this->createResources($this->getReferencesForGlob($query));\n\n        ksort($results);\n\n        return new ArrayResourceCollection(array_values($results));\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function contains($query, $language = 'glob')\n    {\n        if (null === $this->json) {\n            $this->load();\n        }\n\n        $this->failUnlessGlob($language);\n        $query = $this->sanitizePath($query);\n\n        $results = $this->getReferencesForGlob($query, self::STOP_ON_FIRST);\n\n        return !empty($results);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function remove($query, $language = 'glob')\n    {\n        if (null === $this->json) {\n            $this->load();\n        }\n\n        $this->failUnlessGlob($language);\n        $query = $this->sanitizePath($query);\n\n        Assert::notEmpty(trim($query, '/'), 'The root directory cannot be removed.');\n\n        $removed = $this->removeReferences($query);\n\n        $this->flush();\n\n        return $removed;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function clear()\n    {\n        if (null === $this->json) {\n            $this->load();\n        }\n\n        // Subtract root which is not deleted\n        $removed = count($this->getReferencesForRegex('/', '~.~')) - 1;\n\n        $this->json = array();\n\n        $this->flush();\n\n        return $removed;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function listChildren($path)\n    {\n        if (null === $this->json) {\n            $this->load();\n        }\n\n        $path = $this->sanitizePath($path);\n        $results = $this->createResources($this->getReferencesInDirectory($path));\n\n        if (empty($results)) {\n            $pathResults = $this->getReferencesForPath($path);\n\n            if (empty($pathResults)) {\n                throw ResourceNotFoundException::forPath($path);\n            }\n        }\n\n        ksort($results);\n\n        return new ArrayResourceCollection(array_values($results));\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function hasChildren($path)\n    {\n        if (null === $this->json) {\n            $this->load();\n        }\n\n        $path = $this->sanitizePath($path);\n\n        $results = $this->getReferencesInDirectory($path, self::STOP_ON_FIRST);\n\n        if (empty($results)) {\n            $pathResults = $this->getReferencesForPath($path);\n\n            if (empty($pathResults)) {\n                throw ResourceNotFoundException::forPath($path);\n            }\n\n            return false;\n        }\n\n        return true;\n    }\n\n    /**\n     * Inserts a path reference into the JSON file.\n     *\n     * The path reference can be:\n     *\n     *  * a link starting with `@`\n     *  * an absolute filesystem path\n     *\n     * @param string      $path      The Puli path.\n     * @param string|null $reference The path reference.\n     */\n    abstract protected function insertReference($path, $reference);\n\n    /**\n     * Removes all path references matching the given glob from the JSON file.\n     *\n     * @param string $glob The glob for a list of Puli paths.\n     */\n    abstract protected function removeReferences($glob);\n\n    /**\n     * Returns the references for a given Puli path.\n     *\n     * Each reference returned by this method can be:\n     *\n     *  * `null`\n     *  * a link starting with `@`\n     *  * an absolute filesystem path\n     *\n     * The result has either one entry or none, if no path was found. The key\n     * of the single entry is the path passed to this method.\n     *\n     * @param string $path The Puli path.\n     *\n     * @return string[]|null[] A one-level array of references with Puli paths\n     *                         as keys. The array has at most one entry.\n     */\n    abstract protected function getReferencesForPath($path);\n\n    /**\n     * Returns the references matching a given Puli path glob.\n     *\n     * Each reference returned by this method can be:\n     *\n     *  * `null`\n     *  * a link starting with `@`\n     *  * an absolute filesystem path\n     *\n     * The keys of the returned array are Puli paths. Their order is undefined.\n     *\n     * The flag `STOP_ON_FIRST` may be used to stop the search at the first result.\n     *\n     * @param string $glob  The glob.\n     * @param int    $flags A bitwise combination of the flag constants in this\n     *                      class.\n     *\n     * @return string[]|null[] A one-level array of references with Puli paths\n     *                         as keys.\n     */\n    abstract protected function getReferencesForGlob($glob, $flags = 0);\n\n    /**\n     * Returns the references matching a given Puli path regular expression.\n     *\n     * Each reference returned by this method can be:\n     *\n     *  * `null`\n     *  * a link starting with `@`\n     *  * an absolute filesystem path\n     *\n     * The keys of the returned array are Puli paths. Their order is undefined.\n     *\n     * The flag `STOP_ON_FIRST` may be used to stop the search at the first result.\n     *\n     * @param string $staticPrefix The static prefix of all Puli paths matching\n     *                             the regular expression.\n     * @param string $regex        The regular expression.\n     * @param int    $flags        A bitwise combination of the flag constants\n     *                             in this class.\n     *\n     * @return string[]|null[] A one-level array of references with Puli paths\n     *                         as keys.\n     */\n    abstract protected function getReferencesForRegex($staticPrefix, $regex, $flags = 0);\n\n    /**\n     * Returns the references in a given Puli path.\n     *\n     * Each reference returned by this method can be:\n     *\n     *  * `null`\n     *  * a link starting with `@`\n     *  * an absolute filesystem path\n     *\n     * The keys of the returned array are Puli paths. Their order is undefined.\n     *\n     * The flag `STOP_ON_FIRST` may be used to stop the search at the first result.\n     *\n     * @param string $path  The Puli path.\n     * @param int    $flags A bitwise combination of the flag constants in this\n     *                      class.\n     *\n     * @return string[]|null[] A one-level array of references with Puli paths\n     *                         as keys.\n     */\n    abstract protected function getReferencesInDirectory($path, $flags = 0);\n\n    /**\n     * Logs a message.\n     *\n     * @param mixed  $level   One of the level constants in {@link LogLevel}.\n     * @param string $message The message.\n     */\n    protected function log($level, $message)\n    {\n        if (null !== $this->logger) {\n            $this->logger->log($level, $message);\n        }\n    }\n\n    /**\n     * Logs a warning that a reference could not be found.\n     *\n     * @param string $path              The Puli path of a path mapping.\n     * @param string $reference         The reference that was not found.\n     * @param string $absoluteReference The absolute filesystem path of the\n     *                                  reference.\n     */\n    protected function logReferenceNotFound($path, $reference, $absoluteReference)\n    {\n        $this->log(LogLevel::WARNING, sprintf(\n            'The reference \"%s\"%s mapped by the path %s could not be found.',\n            $reference,\n            $reference !== $absoluteReference ? ' ('.$absoluteReference.')' : '',\n            $path\n        ));\n    }\n\n    /**\n     * Adds a filesystem resource to the JSON file.\n     *\n     * @param string             $path     The Puli path.\n     * @param FilesystemResource $resource The resource to add.\n     */\n    protected function addFilesystemResource($path, FilesystemResource $resource)\n    {\n        $resource = clone $resource;\n        $resource->attachTo($this, $path);\n\n        $relativePath = Path::makeRelative($resource->getFilesystemPath(), $this->baseDirectory);\n\n        $this->insertReference($path, $relativePath);\n\n        $this->storeVersion($resource);\n    }\n\n    /**\n     * Loads the JSON file.\n     */\n    protected function load()\n    {\n        $decoder = new JsonDecoder();\n\n        $this->json = file_exists($this->path)\n            ? (array) $decoder->decodeFile($this->path, $this->schemaPath)\n            : array();\n\n        if (isset($this->json['_order'])) {\n            $this->json['_order'] = (array) $this->json['_order'];\n\n            foreach ($this->json['_order'] as $path => $entries) {\n                foreach ($entries as $key => $entry) {\n                    $this->json['_order'][$path][$key] = (array) $entry;\n                }\n            }\n        }\n\n        // The root node always exists\n        if (!isset($this->json['/'])) {\n            $this->json['/'] = null;\n        }\n\n        // Make sure the JSON is sorted in reverse order\n        krsort($this->json);\n    }\n\n    /**\n     * Writes the JSON file.\n     */\n    protected function flush()\n    {\n        // The root node always exists\n        if (!isset($this->json['/'])) {\n            $this->json['/'] = null;\n        }\n\n        // Always save in reverse order\n        krsort($this->json);\n\n        // Comply to schema\n        $json = (object) $this->json;\n\n        if (isset($json->{'_order'})) {\n            $order = $json->{'_order'};\n\n            foreach ($order as $path => $entries) {\n                foreach ($entries as $key => $entry) {\n                    $order[$path][$key] = (object) $entry;\n                }\n            }\n\n            $json->{'_order'} = (object) $order;\n        }\n\n        $this->encoder->encodeFile($json, $this->path, $this->schemaPath);\n    }\n\n    /**\n     * Returns whether a reference contains a link.\n     *\n     * @param string $reference The reference.\n     *\n     * @return bool Whether the reference contains a link.\n     */\n    protected function isLinkReference($reference)\n    {\n        return isset($reference[0]) && '@' === $reference[0];\n    }\n\n    /**\n     * Returns whether a reference contains an absolute or relative filesystem\n     * path.\n     *\n     * @param string $reference The reference.\n     *\n     * @return bool Whether the reference contains a filesystem path.\n     */\n    protected function isFilesystemReference($reference)\n    {\n        return null !== $reference && !$this->isLinkReference($reference);\n    }\n\n    /**\n     * Turns a reference into a resource.\n     *\n     * @param string      $path      The Puli path.\n     * @param string|null $reference The reference.\n     *\n     * @return PuliResource The resource.\n     */\n    protected function createResource($path, $reference)\n    {\n        if (null === $reference) {\n            $resource = new GenericResource();\n        } elseif (isset($reference[0]) && '@' === $reference[0]) {\n            $resource = new LinkResource(substr($reference, 1));\n        } elseif (is_dir($reference)) {\n            $resource = new DirectoryResource($reference);\n        } elseif (is_file($reference)) {\n            $resource = new FileResource($reference);\n        } else {\n            throw new RuntimeException(sprintf(\n                'Trying to create a FilesystemResource on a non-existing file or directory \"%s\"',\n                $reference\n            ));\n        }\n\n        $resource->attachTo($this, $path);\n\n        return $resource;\n    }\n\n    /**\n     * Turns a list of references into a list of resources.\n     *\n     * The references are expected to be in the format returned by\n     * {@link getReferencesForPath()}, {@link getReferencesForGlob()} and\n     * {@link getReferencesInDirectory()}.\n     *\n     * The result contains Puli paths as keys and {@link PuliResource}\n     * implementations as values. The order of the results is undefined.\n     *\n     * @param string[]|null[] $references The references indexed by Puli paths.\n     *\n     * @return array\n     */\n    private function createResources(array $references)\n    {\n        foreach ($references as $path => $reference) {\n            $references[$path] = $this->createResource($path, $reference);\n        }\n\n        return $references;\n    }\n\n    /**\n     * Adds all ancestor directories of a path to the repository.\n     *\n     * @param string $path A Puli path.\n     */\n    private function ensureDirectoryExists($path)\n    {\n        if (array_key_exists($path, $this->json)) {\n            return;\n        }\n\n        // Recursively initialize parent directories\n        if ('/' !== $path) {\n            $this->ensureDirectoryExists(Path::getDirectory($path));\n        }\n\n        $this->json[$path] = null;\n    }\n\n    /**\n     * Adds a resource to the repository.\n     *\n     * @param string                          $path     The Puli path to add the\n     *                                                  resource at.\n     * @param FilesystemResource|LinkResource $resource The resource to add.\n     */\n    private function addResource($path, $resource)\n    {\n        if (!$resource instanceof FilesystemResource && !$resource instanceof LinkResource) {\n            throw new UnsupportedResourceException(sprintf(\n                'The %s only supports adding FilesystemResource and '.\n                'LinkedResource instances. Got: %s',\n                // Get the short class name\n                $this->getShortClassName(get_class($this)),\n                $this->getShortClassName(get_class($resource))\n            ));\n        }\n\n        if ($resource instanceof LinkResource) {\n            $resource = clone $resource;\n            $resource->attachTo($this, $path);\n\n            $this->insertReference($path, '@'.$resource->getTargetPath());\n\n            $this->storeVersion($resource);\n        } else {\n            // Extension point for the optimized repository\n            $this->addFilesystemResource($path, $resource);\n        }\n    }\n\n    /**\n     * Returns the short name of a fully-qualified class name.\n     *\n     * @param string $className The fully-qualified class name.\n     *\n     * @return string The short class name.\n     */\n    private function getShortClassName($className)\n    {\n        if (false !== ($pos = strrpos($className, '\\\\'))) {\n            return substr($className, $pos + 1);\n        }\n\n        return $className;\n    }\n}\n"
  },
  {
    "path": "src/AbstractRepository.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository;\n\nuse Puli\\Repository\\Api\\ChangeStream\\VersionList;\nuse Puli\\Repository\\Api\\NoVersionFoundException;\nuse Puli\\Repository\\Api\\ResourceNotFoundException;\nuse Puli\\Repository\\Api\\ResourceRepository;\nuse Puli\\Repository\\Api\\UnsupportedLanguageException;\nuse Webmozart\\Assert\\Assert;\nuse Webmozart\\PathUtil\\Path;\n\n/**\n * Abstract base for repositories providing tools to avoid code duplication.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n * @author Titouan Galopin <galopintitouan@gmail.com>\n */\nabstract class AbstractRepository implements ResourceRepository\n{\n    /**\n     * {@inheritdoc}\n     */\n    public function getVersions($path)\n    {\n        // Non-editable repositories always contain only one version of a resource\n        try {\n            return new VersionList($path, array($this->get($path)));\n        } catch (ResourceNotFoundException $e) {\n            throw NoVersionFoundException::forPath($path, $e);\n        }\n    }\n\n    /**\n     * Validate a language is usable to search in repositories.\n     *\n     * @param string $language\n     */\n    protected function failUnlessGlob($language)\n    {\n        if ('glob' !== $language) {\n            throw UnsupportedLanguageException::forLanguage($language);\n        }\n    }\n\n    /**\n     * Sanitize a given path and check its validity.\n     *\n     * @param string $path\n     *\n     * @return string\n     */\n    protected function sanitizePath($path)\n    {\n        Assert::stringNotEmpty($path, 'The path must be a non-empty string. Got: %s');\n        Assert::startsWith($path, '/', 'The path %s is not absolute.');\n\n        return Path::canonicalize($path);\n    }\n}\n"
  },
  {
    "path": "src/Api/ChangeStream/ChangeStream.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Api\\ChangeStream;\n\nuse Puli\\Repository\\Api\\NoVersionFoundException;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\nuse Puli\\Repository\\Api\\ResourceRepository;\n\n/**\n * Tracks different versions of a resource.\n *\n * @since  1.0\n *\n * @author Titouan Galopin <galopintitouan@gmail.com>\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\ninterface ChangeStream\n{\n    /**\n     * Stores a new version of a resource.\n     *\n     * @param PuliResource $resource The resource to store.\n     */\n    public function append(PuliResource $resource);\n\n    /**\n     * Removes all versions stored for a path.\n     *\n     * @param string $path The Puli path.\n     */\n    public function purge($path);\n\n    /**\n     * Returns whether the stream contains any version for a path.\n     *\n     * @param string $path The Puli path.\n     *\n     * @return bool Returns `true` if a version can be found and `false` otherwise.\n     */\n    public function contains($path);\n\n    /**\n     * Removes all contents of the stream.\n     */\n    public function clear();\n\n    /**\n     * Returns all versions of a resource.\n     *\n     * @param string             $path       The Puli path to look for.\n     * @param ResourceRepository $repository The repository to attach the\n     *                                       resources to.\n     *\n     * @return VersionList The versions of the resource.\n     *\n     * @throws NoVersionFoundException If no version is found for the path.\n     */\n    public function getVersions($path, ResourceRepository $repository = null);\n}\n"
  },
  {
    "path": "src/Api/ChangeStream/VersionList.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Api\\ChangeStream;\n\nuse ArrayAccess;\nuse ArrayIterator;\nuse BadMethodCallException;\nuse Countable;\nuse IteratorAggregate;\nuse OutOfBoundsException;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\nuse Webmozart\\Assert\\Assert;\n\n/**\n * Contains different versions of a resource.\n *\n * @since  1.0\n *\n * @author Titouan Galopin <galopintitouan@gmail.com>\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass VersionList implements IteratorAggregate, ArrayAccess, Countable\n{\n    /**\n     * @var string\n     */\n    private $path;\n\n    /**\n     * @var array\n     */\n    private $versions;\n\n    /**\n     * Creates a new version list.\n     *\n     * @param string         $path     The Puli path.\n     * @param PuliResource[] $versions The versions of the resource, starting\n     *                                 with the first.\n     */\n    public function __construct($path, array $versions)\n    {\n        Assert::stringNotEmpty($path, 'The Puli path must be a non-empty string. Got: %s');\n        Assert::allIsInstanceOf($versions, 'Puli\\Repository\\Api\\Resource\\PuliResource');\n        Assert::greaterThanEq(count($versions), 1, 'Expected at least one version.');\n\n        $this->path = $path;\n        $this->versions = array_values($versions);\n    }\n\n    /**\n     * Returns the path of the versioned resources.\n     *\n     * @return string The Puli path.\n     */\n    public function getPath()\n    {\n        return $this->path;\n    }\n\n    /**\n     * Returns the current version of the resource.\n     *\n     * @return PuliResource The current version.\n     */\n    public function getCurrent()\n    {\n        return $this->get($this->getCurrentVersion());\n    }\n\n    /**\n     * Returns the current version number.\n     *\n     * @return int The current version number.\n     */\n    public function getCurrentVersion()\n    {\n        return count($this->versions) - 1;\n    }\n\n    /**\n     * Returns the first version of the resource.\n     *\n     * @return PuliResource The first version.\n     */\n    public function getFirst()\n    {\n        return $this->get($this->getFirstVersion());\n    }\n\n    /**\n     * Returns the first version number.\n     *\n     * @return int The first version number.\n     */\n    public function getFirstVersion()\n    {\n        return 0;\n    }\n\n    /**\n     * Returns whether a specific version exists.\n     *\n     * @param int $version The version number starting at 0.\n     *\n     * @return bool Whether the version exists.\n     */\n    public function contains($version)\n    {\n        return isset($this->versions[$version]);\n    }\n\n    /**\n     * Returns a specific version of the resource.\n     *\n     * @param int $version The version number starting at 0.\n     *\n     * @return PuliResource The resource.\n     *\n     * @throws OutOfBoundsException If the version number does not exist.\n     */\n    public function get($version)\n    {\n        if (!isset($this->versions[$version])) {\n            throw new OutOfBoundsException(sprintf(\n                'The version %s of path %s does not exist.',\n                $version,\n                $this->path\n            ));\n        }\n\n        return $this->versions[$version];\n    }\n\n    /**\n     * Returns all version numbers.\n     *\n     * @return int[] The version numbers.\n     */\n    public function getVersions()\n    {\n        return array_keys($this->versions);\n    }\n\n    /**\n     * Returns the list as array indexed by version numbers.\n     *\n     * @return PuliResource[] The resource versions indexed by their version numbers.\n     */\n    public function toArray()\n    {\n        return $this->versions;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getIterator()\n    {\n        return new ArrayIterator($this->versions);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function offsetExists($offset)\n    {\n        return $this->contains($offset);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function offsetGet($offset)\n    {\n        return $this->get($offset);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function offsetSet($offset, $value)\n    {\n        throw new BadMethodCallException('List entries may not be changed.');\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function offsetUnset($offset)\n    {\n        throw new BadMethodCallException('List entries may not be removed.');\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function count()\n    {\n        return count($this->versions);\n    }\n}\n"
  },
  {
    "path": "src/Api/EditableRepository.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Api;\n\nuse InvalidArgumentException;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\n\n/**\n * A repository that supports the addition and removal of resources.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\ninterface EditableRepository extends ResourceRepository\n{\n    /**\n     * Adds a new resource to the repository.\n     *\n     * All resources passed to this method must implement {@link PuliResource}.\n     *\n     * @param string                          $path     The path at which to\n     *                                                  add the resource.\n     * @param PuliResource|ResourceCollection $resource The resource(s) to add\n     *                                                  at that path.\n     *\n     * @throws InvalidArgumentException     If the path is invalid. The path\n     *                                      must be  a non-empty string starting\n     *                                      with \"/\".\n     * @throws UnsupportedResourceException If the resource is invalid.\n     */\n    public function add($path, $resource);\n\n    /**\n     * Removes all resources matching the given query.\n     *\n     * @param string $query    A resource query.\n     * @param string $language The language of the query. All implementations\n     *                         must support the language \"glob\".\n     *\n     * @return int The number of resources removed from the repository.\n     *\n     * @throws InvalidArgumentException     If the query is invalid.\n     * @throws UnsupportedLanguageException If the language is not supported.\n     */\n    public function remove($query, $language = 'glob');\n\n    /**\n     * Removes all resources from the repository.\n     *\n     * @return int The number of resources removed from the repository.\n     */\n    public function clear();\n}\n"
  },
  {
    "path": "src/Api/NoVersionFoundException.php",
    "content": "<?php\n\n/*\n * This file is part of the vendor/project package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Api;\n\nuse Exception;\n\n/**\n * Thrown when a change stream contains no version of a resource.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass NoVersionFoundException extends ResourceNotFoundException\n{\n    /**\n     * Creates a new exception for a resource path.\n     *\n     * @param string         $path  The path which was not found.\n     * @param Exception|null $cause The exception that caused this exception.\n     *\n     * @return static The created exception.\n     */\n    public static function forPath($path, Exception $cause = null)\n    {\n        return new static(sprintf(\n            'Could not find any version of path %s.',\n            $path\n        ), 0, $cause);\n    }\n}\n"
  },
  {
    "path": "src/Api/Resource/BodyResource.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Api\\Resource;\n\n/**\n * A resource that contains a body.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\ninterface BodyResource extends PuliResource\n{\n    /**\n     * Returns the body of the resource.\n     *\n     * @return string The resource body.\n     */\n    public function getBody();\n}\n"
  },
  {
    "path": "src/Api/Resource/FilesystemResource.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Api\\Resource;\n\n/**\n * A resource associated to a file on the file system.\n *\n * The path of the file can be accessed with {@link getFilesystemPath}.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\ninterface FilesystemResource extends PuliResource\n{\n    /**\n     * Returns the path on the file system.\n     *\n     * @return string|null The file system path or `null` if the resource has no\n     *                     associated local file.\n     */\n    public function getFilesystemPath();\n}\n"
  },
  {
    "path": "src/Api/Resource/PuliResource.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Api\\Resource;\n\nuse Puli\\Repository\\Api\\ChangeStream\\VersionList;\nuse Puli\\Repository\\Api\\ResourceCollection;\nuse Puli\\Repository\\Api\\ResourceNotFoundException;\nuse Puli\\Repository\\Api\\ResourceRepository;\nuse Serializable;\n\n/**\n * A resource.\n *\n * Resources are objects which can be stored in a resource repository. Resources\n * have a path, under which they are stored in the repository.\n *\n * Depending on the implementation, resources may offer additional functionality:\n *\n *  * Resources that are similar to files in that they have a body and a size\n *    should implement {@link BodyResource}.\n *\n * Resources can be attached to a repository by calling {@link attachTo()}. They\n * can be detached again by calling {@link detach()}. Use {@link isAttached()}\n * to find out whether a resource is attached and {@link getRepository()} to\n * obtain the attached repository.\n *\n * You can create a reference to a resource by calling {@link createReference()}.\n * References can have different paths than the resource they are referencing.\n * Otherwise, they are identical to the referenced resource. Use\n * {@link isReference()} to check whether a resource is a reference. You can\n * call {@link getRepositoryPath()} to retrieve the path of the referenced\n * resource.\n *\n * If you implement a custom resource, let your test extend\n * {@link AbstractResourceTest} to make sure your resource satisfies the\n * constraints of the interface. Extend {@link GenericResource} if you want to\n * avoid reimplementing basic functionality.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\ninterface PuliResource extends Serializable\n{\n    /**\n     * Returns the path of the resource.\n     *\n     * For references created with {@link createReference()}, the path returned\n     * by this method is the reference path and not the actual repository path\n     * of the referenced resource. You should use {@link getRepositoryPath()} if\n     * you want to glob the repository for a resource.\n     *\n     * @return string|null The path of the resource. If the resource has no\n     *                     path, `null` is returned.\n     */\n    public function getPath();\n\n    /**\n     * Returns the name of the resource.\n     *\n     * The name is the last segment of the path returned by {@link getPath()}.\n     *\n     * @return string|null The name of the resource. If the resource has no\n     *                     path, `null` is returned.\n     */\n    public function getName();\n\n    /**\n     * Returns the child resource with the given relative path.\n     *\n     * \".\" and \"..\" are supported as paths.\n     *\n     * @param string $relPath The relative resource path.\n     *\n     * @return PuliResource The resource with the given path.\n     *\n     * @throws ResourceNotFoundException If the resource cannot be found.\n     */\n    public function getChild($relPath);\n\n    /**\n     * Returns whether the child resource with the given relative path exists.\n     *\n     * @param string $relPath The relative resource path.\n     *\n     * @return bool Whether a resource with the given path exists.\n     */\n    public function hasChild($relPath);\n\n    /**\n     * Returns whether the resource has child resources.\n     *\n     * @return bool Returns `true` if the resource has child resources.\n     */\n    public function hasChildren();\n\n    /**\n     * Lists the child resources of the resources.\n     *\n     * @return ResourceCollection The child resources indexed by their names.\n     */\n    public function listChildren();\n\n    /**\n     * Returns the versions of this resource.\n     *\n     * @return VersionList The resource versions.\n     */\n    public function getVersions();\n\n    /**\n     * Returns metadata about a resource.\n     *\n     * @return ResourceMetadata The resource metadata.\n     */\n    public function getMetadata();\n\n    /**\n     * Returns the repository that the resource is attached to.\n     *\n     * Use {@link attachTo()} to attach a resource to a repository. The method\n     * {@link detach()} can be used to detach an attached resource.\n     *\n     * @return ResourceRepository|null The resource repository. If the resource\n     *                                 is not attached to any repository, `null`\n     *                                 is returned.\n     */\n    public function getRepository();\n\n    /**\n     * Returns the path of the resource in the repository.\n     *\n     * The repository path is the path that the resource is mapped to once\n     * attached to a repository. The result of this method is different from\n     * {@link getPath()} for resource references. PuliResource references return\n     * the path of the referenced resource here, while {@link getPath()} returns\n     * the path of the reference itself.\n     *\n     * @return string|null The repository path of the resource. If the resource\n     *                     has no repository path, `null` is returned.\n     */\n    public function getRepositoryPath();\n\n    /**\n     * Attaches the resource to a repository.\n     *\n     * You can optionally change the path of the resource by passing it in the\n     * second argument. Beware that this may break the resource if it is still\n     * referenced by another repository. Hence you should clone resources that\n     * are attached to another repository before attaching them:\n     *\n     * ```php\n     * if ($resource->isAttached()) {\n     *     $resource = clone $resource;\n     * }\n     *\n     * $resource->attachTo($repo, '/path/in/repo');\n     * ```\n     *\n     * @param ResourceRepository $repo The repository.\n     * @param string|null        $path The path of the resource in the\n     *                                 repository. If not passed, the resource\n     *                                 will be attached to it current path.\n     */\n    public function attachTo(ResourceRepository $repo, $path = null);\n\n    /**\n     * Detaches the resource from the repository.\n     *\n     * After calling this method, {@link isAttached()} returns `false`. The\n     * method {@link getRepository()} should return `null` after detaching.\n     *\n     * Neither the path nor the repository path of the resource should be\n     * modified when detaching.\n     */\n    public function detach();\n\n    /**\n     * Returns whether the resource is attached to a repository.\n     *\n     * Resources can be attached to a repository with {@link attachTo()}. The\n     * method {@link getRepository()} returns the attached repository.\n     *\n     * @return bool Whether the resource is attached to a repository.\n     */\n    public function isAttached();\n\n    /**\n     * Creates a reference to the resource.\n     *\n     * References are identical for their referenced resource except for their\n     * path. The path of the referenced resource can be obtained by calling\n     * {@link getRepositoryPath()}:\n     *\n     * ```php\n     * $resource = new MyResource('/path');\n     * $reference = $resource->createReference('/reference');\n     *\n     * $reference->getPath();\n     * // \"/reference\"\n     *\n     * $reference->getRepositoryPath();\n     * // \"/path\"\n     * ```\n     *\n     * Use {@link isReference()} to find out whether a resource is a reference.\n     *\n     * @param string $path The path of the reference.\n     *\n     * @return static The reference.\n     */\n    public function createReference($path);\n\n    /**\n     * Returns whether a resource is a reference.\n     *\n     * References are created by calling {@link createReference()}.\n     *\n     * @return bool Whether the resource is a reference.\n     */\n    public function isReference();\n}\n"
  },
  {
    "path": "src/Api/Resource/ResourceMetadata.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Api\\Resource;\n\n/**\n * Contains metadata about a resource.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass ResourceMetadata\n{\n    /**\n     * Returns when the resource was created.\n     *\n     * If this information is not available, the method returns 0.\n     *\n     * @return int A UNIX timestamp.\n     */\n    public function getCreationTime()\n    {\n        return 0;\n    }\n\n    /**\n     * Returns when the resource was last accessed.\n     *\n     * If this information is not available, the method returns 0.\n     *\n     * @return int A UNIX timestamp.\n     */\n    public function getAccessTime()\n    {\n        return 0;\n    }\n\n    /**\n     * Returns when the resource was last modified.\n     *\n     * If this information is not available, the method returns 0.\n     *\n     * @return int A UNIX timestamp.\n     */\n    public function getModificationTime()\n    {\n        return 0;\n    }\n\n    /**\n     * Returns the size of the body in bytes.\n     *\n     * If this information is not available, the method returns 0.\n     *\n     * @return int The body size in bytes.\n     */\n    public function getSize()\n    {\n        return 0;\n    }\n}\n"
  },
  {
    "path": "src/Api/ResourceCollection.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Api;\n\nuse ArrayAccess;\nuse Countable;\nuse InvalidArgumentException;\nuse OutOfBoundsException;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\nuse Traversable;\n\n/**\n * A collection of {@link PuliResource} instances.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\ninterface ResourceCollection extends Traversable, ArrayAccess, Countable\n{\n    /**\n     * Adds a resource to the collection.\n     *\n     * @param PuliResource $resource The added resource.\n     */\n    public function add(PuliResource $resource);\n\n    /**\n     * Sets a resource at a collection key.\n     *\n     * @param int          $key      The collection key.\n     * @param PuliResource $resource The resource to set.\n     */\n    public function set($key, PuliResource $resource);\n\n    /**\n     * Returns the resource for a collection key.\n     *\n     * @param int $key The collection key.\n     *\n     * @return PuliResource The resource at the key.\n     *\n     * @throws OutOfBoundsException If the key does not exist.\n     */\n    public function get($key);\n\n    /**\n     * Removes a collection key from the collection.\n     *\n     * @param int $key The collection key.\n     */\n    public function remove($key);\n\n    /**\n     * Returns whether a collection key exists.\n     *\n     * @param int $key The collection key.\n     *\n     * @return bool Whether the collection key exists.\n     */\n    public function has($key);\n\n    /**\n     * Removes all resources from the collection.\n     */\n    public function clear();\n\n    /**\n     * Returns the keys of the collection.\n     *\n     * @return int[] The collection keys.\n     */\n    public function keys();\n\n    /**\n     * Replaces the collection contents with the given resources.\n     *\n     * @param PuliResource[]|Traversable $resources The resources to write into\n     *                                              the collection.\n     *\n     * @throws InvalidArgumentException     If the resources are not an array and\n     *                                      not a traversable object.\n     * @throws UnsupportedResourceException If a resource does not implement\n     *                                      {@link PuliResource}.\n     */\n    public function replace($resources);\n\n    /**\n     * Merges the given resources into the collection.\n     *\n     * @param PuliResource[]|Traversable $resources The resources to merge into\n     *                                              the collection.\n     *\n     * @throws InvalidArgumentException     If the resources are not an array\n     *                                      and not a traversable object.\n     * @throws UnsupportedResourceException If a resource does not implement\n     *                                      {@link PuliResource}.\n     */\n    public function merge($resources);\n\n    /**\n     * Returns whether the collection is empty.\n     *\n     * @return bool Returns `true` only if the collection contains no resources.\n     */\n    public function isEmpty();\n\n    /**\n     * Returns the paths of all resources in the collection.\n     *\n     * The paths are returned in the order of their resources in the collection.\n     *\n     * @return string[] The paths of the resources in the collection.\n     *\n     * @see PuliResource::getPath\n     */\n    public function getPaths();\n\n    /**\n     * Returns the names of all resources in the collection.\n     *\n     * The names are returned in the order of their resources in the collection.\n     *\n     * @return string[] The names of the resources in the collection.\n     *\n     * @see PuliResource::getName\n     */\n    public function getNames();\n\n    /**\n     * Returns the collection contents as array.\n     *\n     * @return PuliResource[] The resources in the collection.\n     */\n    public function toArray();\n}\n"
  },
  {
    "path": "src/Api/ResourceIterator.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Api;\n\nuse Iterator;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\n\n/**\n * An iterator over {@link PuliResource} objects.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\ninterface ResourceIterator extends Iterator\n{\n    /**\n     * Returns the resource at the current position of the iterator.\n     *\n     * @return PuliResource The resource at the current position.\n     */\n    public function getCurrentResource();\n}\n"
  },
  {
    "path": "src/Api/ResourceNotFoundException.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Api;\n\nuse Exception;\nuse RuntimeException;\n\n/**\n * Thrown when a requested resource was not found.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass ResourceNotFoundException extends RuntimeException\n{\n    /**\n     * Creates a new exception for a resource path.\n     *\n     * @param string         $path  The path which was not found.\n     * @param Exception|null $cause The exception that caused this exception.\n     *\n     * @return static The created exception.\n     */\n    public static function forPath($path, Exception $cause = null)\n    {\n        return new static(sprintf(\n            'The resource %s does not exist.',\n            $path\n        ), 0, $cause);\n    }\n}\n"
  },
  {
    "path": "src/Api/ResourceRepository.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Api;\n\nuse InvalidArgumentException;\nuse Puli\\Repository\\Api\\ChangeStream\\VersionList;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\n\n/**\n * Stores {@link PuliResource} objects.\n *\n * A resource repository is similar to a filesystem. It stores {@link PuliResource}\n * objects, each of which has a path in the repository:\n *\n * ```php\n * $resource = $repo->get('/css/style.css');\n * ```\n *\n * Resources may have child resources. These can be accessed with\n * {@link listChildren()}:\n *\n * ```php\n * $resource = $repo->get('/css');\n *\n * foreach ($resource->listChildren() as $name => $resource) {\n *     // ...\n * }\n * ```\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n * @author Titouan Galopin <galopintitouan@gmail.com>\n */\ninterface ResourceRepository\n{\n    /**\n     * Returns the resource at the given path.\n     *\n     * @param string $path The path to the resource. Must start with \"/\". \".\"\n     *                     and \"..\" segments in the path are supported.\n     *\n     * @return PuliResource The resource at this path.\n     *\n     * @throws ResourceNotFoundException If the resource cannot be found.\n     * @throws InvalidArgumentException  If the path is invalid. The path must\n     *                                   be a non-empty string starting with \"/\".\n     */\n    public function get($path);\n\n    /**\n     * Returns all versions of a resource.\n     *\n     * @param string $path The path to the resource.\n     *\n     * @return VersionList The versions stored for this path.\n     *\n     * @throws NoVersionFoundException  If no version can be found.\n     * @throws InvalidArgumentException If the path is invalid. The path must\n     *                                  be a non-empty string starting with \"/\".\n     */\n    public function getVersions($path);\n\n    /**\n     * Returns the resources matching a query.\n     *\n     * @param string $query    A resource query.\n     * @param string $language The language of the query. All implementations\n     *                         must support the language \"glob\".\n     *\n     * @return ResourceCollection The resources matching the query.\n     *\n     * @throws InvalidArgumentException     If the query is invalid.\n     * @throws UnsupportedLanguageException If the language is not supported.\n     */\n    public function find($query, $language = 'glob');\n\n    /**\n     * Returns whether any resources match a query.\n     *\n     * @param string $query    A resource query.\n     * @param string $language The language of the query. All implementations\n     *                         must support the language \"glob\".\n     *\n     * @return bool Returns `true` if any resources exist that match the query.\n     *\n     * @throws InvalidArgumentException     If the query is invalid.\n     * @throws UnsupportedLanguageException If the language is not supported.\n     */\n    public function contains($query, $language = 'glob');\n\n    /**\n     * Returns whether a resource has child resources.\n     *\n     * @param string $path The path to the resource. Must start with \"/\".\n     *                     \".\" and \"..\" segments in the path are supported.\n     *\n     * @return bool Returns `true` if the resource has child resources.\n     *\n     * @throws ResourceNotFoundException If the resource cannot be found.\n     * @throws InvalidArgumentException  If the path is invalid. The path must\n     *                                   be a non-empty string starting with \"/\".\n     */\n    public function hasChildren($path);\n\n    /**\n     * Lists the child resources of a resource.\n     *\n     * @param string $path The path to the resource. Must start with \"/\".\n     *                     \".\" and \"..\" segments in the path are supported.\n     *\n     * @return ResourceCollection The child resources of the resource.\n     *\n     * @throws ResourceNotFoundException If the resource cannot be found.\n     * @throws InvalidArgumentException  If the path is invalid. The path must\n     *                                   be a non-empty string starting with \"/\".\n     */\n    public function listChildren($path);\n}\n"
  },
  {
    "path": "src/Api/UnsupportedLanguageException.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Api;\n\nuse Exception;\nuse RuntimeException;\n\n/**\n * Thrown when a glob language is not supported by the repository.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass UnsupportedLanguageException extends RuntimeException\n{\n    /**\n     * Creates an exception for an unsupported language string.\n     *\n     * @param string         $language The unsupported language.\n     * @param Exception|null $cause    The exception that caused this exception.\n     *\n     * @return static The created exception.\n     */\n    public static function forLanguage($language, Exception $cause = null)\n    {\n        return new static(sprintf(\n            'The language \"%s\" is not supported.',\n            $language\n        ), 0, $cause);\n    }\n}\n"
  },
  {
    "path": "src/Api/UnsupportedOperationException.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Api;\n\nuse RuntimeException;\n\n/**\n * Thrown when the requested operation is not supported by the repository.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass UnsupportedOperationException extends RuntimeException\n{\n}\n"
  },
  {
    "path": "src/Api/UnsupportedResourceException.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Api;\n\nuse RuntimeException;\n\n/**\n * Thrown when a specific implementation of {@link Resource\\PuliResource} is not accepted by\n * the invoked method.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass UnsupportedResourceException extends RuntimeException\n{\n}\n"
  },
  {
    "path": "src/ChangeStream/InMemoryChangeStream.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\ChangeStream;\n\nuse Puli\\Repository\\Api\\ChangeStream\\ChangeStream;\nuse Puli\\Repository\\Api\\ChangeStream\\VersionList;\nuse Puli\\Repository\\Api\\NoVersionFoundException;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\nuse Puli\\Repository\\Api\\ResourceRepository;\n\n/**\n * A change stream stored in memory.\n *\n * @since  1.0\n *\n * @author Titouan Galopin <galopintitouan@gmail.com>\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass InMemoryChangeStream implements ChangeStream\n{\n    /**\n     * @var array\n     */\n    private $versions = array();\n\n    /**\n     * {@inheritdoc}\n     */\n    public function append(PuliResource $resource)\n    {\n        if (!isset($this->versions[$resource->getPath()])) {\n            $this->versions[$resource->getPath()] = array();\n        }\n\n        $this->versions[$resource->getPath()][] = $resource;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function purge($path)\n    {\n        unset($this->versions[$path]);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function clear()\n    {\n        $this->versions = array();\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function contains($path)\n    {\n        return isset($this->versions[$path]);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getVersions($path, ResourceRepository $repository = null)\n    {\n        if (!isset($this->versions[$path])) {\n            throw NoVersionFoundException::forPath($path);\n        }\n\n        $versions = array();\n\n        foreach ($this->versions[$path] as $resource) {\n            if (null !== $repository) {\n                $resource = clone $resource;\n                $resource->attachTo($repository, $path);\n            }\n\n            $versions[] = $resource;\n        }\n\n        return new VersionList($path, $versions);\n    }\n}\n"
  },
  {
    "path": "src/ChangeStream/JsonChangeStream.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\ChangeStream;\n\nuse Puli\\Repository\\Api\\ChangeStream\\ChangeStream;\nuse Puli\\Repository\\Api\\ChangeStream\\VersionList;\nuse Puli\\Repository\\Api\\NoVersionFoundException;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\nuse Puli\\Repository\\Api\\ResourceRepository;\nuse Webmozart\\Json\\JsonDecoder;\nuse Webmozart\\Json\\JsonEncoder;\n\n/**\n * A change stream backed by a JSON file.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass JsonChangeStream implements ChangeStream\n{\n    /**\n     * @var string\n     */\n    private $path;\n\n    /**\n     * @var array\n     */\n    private $json;\n\n    /**\n     * @var JsonEncoder\n     */\n    private $encoder;\n\n    /**\n     * @param string $path The path to the JSON file.\n     */\n    public function __construct($path)\n    {\n        $this->path = $path;\n        $this->encoder = new JsonEncoder();\n        $this->encoder->setPrettyPrinting(true);\n        $this->encoder->setEscapeSlash(false);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function append(PuliResource $resource)\n    {\n        if (null === $this->json) {\n            $this->load();\n        }\n\n        if (!isset($this->json[$resource->getPath()])) {\n            $this->json[$resource->getPath()] = array();\n        }\n\n        $this->json[$resource->getPath()][] = serialize($resource);\n\n        $this->flush();\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function purge($path)\n    {\n        if (null === $this->json) {\n            $this->load();\n        }\n\n        unset($this->json[$path]);\n\n        $this->flush();\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function clear()\n    {\n        if (null === $this->json) {\n            $this->load();\n        }\n\n        $this->json = array();\n\n        $this->flush();\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function contains($path)\n    {\n        if (null === $this->json) {\n            $this->load();\n        }\n\n        return isset($this->json[$path]);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getVersions($path, ResourceRepository $repository = null)\n    {\n        if (null === $this->json) {\n            $this->load();\n        }\n\n        if (!isset($this->json[$path])) {\n            throw NoVersionFoundException::forPath($path);\n        }\n\n        $versions = array();\n\n        foreach ($this->json[$path] as $resource) {\n            $resource = unserialize($resource);\n\n            if (null !== $repository) {\n                $resource->attachTo($repository, $path);\n            }\n\n            $versions[] = $resource;\n        }\n\n        return new VersionList($path, $versions);\n    }\n\n    /**\n     * Loads the JSON file.\n     */\n    private function load()\n    {\n        $decoder = new JsonDecoder();\n        $decoder->setObjectDecoding(JsonDecoder::ASSOC_ARRAY);\n\n        $this->json = file_exists($this->path)\n            ? $decoder->decodeFile($this->path)\n            : array();\n    }\n\n    /**\n     * Writes the JSON file.\n     */\n    private function flush()\n    {\n        $this->encoder->encodeFile($this->json, $this->path);\n    }\n}\n"
  },
  {
    "path": "src/ChangeStream/KeyValueStoreChangeStream.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\ChangeStream;\n\nuse Puli\\Repository\\Api\\ChangeStream\\ChangeStream;\nuse Puli\\Repository\\Api\\ChangeStream\\VersionList;\nuse Puli\\Repository\\Api\\NoVersionFoundException;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\nuse Puli\\Repository\\Api\\ResourceRepository;\nuse Webmozart\\KeyValueStore\\Api\\KeyValueStore;\n\n/**\n * A change stream backed by a key-value store.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass KeyValueStoreChangeStream implements ChangeStream\n{\n    /**\n     * @var KeyValueStore\n     */\n    private $store;\n\n    /**\n     * @param KeyValueStore $store\n     */\n    public function __construct(KeyValueStore $store)\n    {\n        $this->store = $store;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function append(PuliResource $resource)\n    {\n        $versions = $this->store->get($resource->getPath(), array());\n\n        $versions[] = $resource;\n\n        $this->store->set($resource->getPath(), $versions);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function purge($path)\n    {\n        $this->store->remove($path);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function contains($path)\n    {\n        return $this->store->exists($path);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function clear()\n    {\n        $this->store->clear();\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getVersions($path, ResourceRepository $repository = null)\n    {\n        $versions = $this->store->get($path, array());\n\n        if (empty($versions)) {\n            throw NoVersionFoundException::forPath($path);\n        }\n\n        if (null !== $repository) {\n            foreach ($versions as $key => $resource) {\n                $resource = clone $resource;\n                $resource->attachTo($repository);\n                $versions[$key] = $resource;\n            }\n        }\n\n        return new VersionList($path, $versions);\n    }\n}\n"
  },
  {
    "path": "src/Discovery/ResourceBinding.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Discovery;\n\nuse Puli\\Discovery\\Api\\Binding\\Binding;\nuse Puli\\Discovery\\Api\\Binding\\Initializer\\NotInitializedException;\nuse Puli\\Discovery\\Api\\Type\\MissingParameterException;\nuse Puli\\Discovery\\Api\\Type\\NoSuchParameterException;\nuse Puli\\Discovery\\Binding\\AbstractBinding;\nuse Puli\\Repository\\Api\\ResourceCollection;\nuse Puli\\Repository\\Api\\ResourceRepository;\n\n/**\n * Binds one or more resources to a binding type.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass ResourceBinding extends AbstractBinding\n{\n    /**\n     * @var string\n     */\n    private $query;\n\n    /**\n     * @var string\n     */\n    private $language;\n\n    /**\n     * @var ResourceRepository\n     */\n    private $repo;\n\n    /**\n     * Creates a new resource binding.\n     *\n     * A resource binding has a query that is used to retrieve the resources\n     * matched by the binding.\n     *\n     * You can pass parameters that have been defined for the type. If you pass\n     * unknown parameters, or if a required parameter is missing, an exception\n     * is thrown.\n     *\n     * All parameters that you do not set here will receive the default values\n     * set for the parameter.\n     *\n     * @param string $query           The resource query.\n     * @param string $typeName        The type to bind against.\n     * @param array  $parameterValues The values of the parameters defined\n     *                                for the type.\n     * @param string $language        The language of the resource query.\n     *\n     * @throws NoSuchParameterException  If an invalid parameter was passed.\n     * @throws MissingParameterException If a required parameter was not passed.\n     */\n    public function __construct($query, $typeName, array $parameterValues = array(), $language = 'glob')\n    {\n        parent::__construct($typeName, $parameterValues);\n\n        $this->query = $query;\n        $this->language = $language;\n    }\n\n    /**\n     * Returns the query for the resources of the binding.\n     *\n     * @return string The resource query.\n     */\n    public function getQuery()\n    {\n        return $this->query;\n    }\n\n    /**\n     * Returns the language of the query.\n     *\n     * @return string The query language.\n     */\n    public function getLanguage()\n    {\n        return $this->language;\n    }\n\n    /**\n     * Returns the bound resources.\n     *\n     * @return ResourceCollection The bound resources.\n     */\n    public function getResources()\n    {\n        if (null === $this->repo) {\n            throw new NotInitializedException('The repository of the resource binding must be set before accessing resources.');\n        }\n\n        return $this->repo->find($this->query, $this->language);\n    }\n\n    /**\n     * Sets the repository used to load resources.\n     *\n     * @param ResourceRepository $repo The resource repository.\n     */\n    public function setRepository(ResourceRepository $repo)\n    {\n        $this->repo = $repo;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function equals(Binding $other)\n    {\n        if (!parent::equals($other)) {\n            return false;\n        }\n\n        /** @var ResourceBinding $other */\n        if ($this->query !== $other->query) {\n            return false;\n        }\n\n        return $this->language === $other->language;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    protected function preSerialize(array &$data)\n    {\n        parent::preSerialize($data);\n\n        $data[] = $this->query;\n        $data[] = $this->language;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    protected function postUnserialize(array &$data)\n    {\n        $this->language = array_pop($data);\n        $this->query = array_pop($data);\n\n        parent::postUnserialize($data);\n    }\n}\n"
  },
  {
    "path": "src/Discovery/ResourceBindingInitializer.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Discovery;\n\nuse Puli\\Discovery\\Api\\Binding\\Binding;\nuse Puli\\Discovery\\Api\\Binding\\Initializer\\BindingInitializer;\nuse Puli\\Repository\\Api\\ResourceRepository;\nuse Webmozart\\Assert\\Assert;\n\n/**\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass ResourceBindingInitializer implements BindingInitializer\n{\n    /**\n     * @var ResourceRepository\n     */\n    private $repo;\n\n    /**\n     * @param ResourceRepository $repo\n     */\n    public function __construct(ResourceRepository $repo)\n    {\n        $this->repo = $repo;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function acceptsBinding($binding)\n    {\n        return $binding instanceof ResourceBinding\n            || $binding === 'Puli\\Repository\\Discovery\\ResourceBinding'\n            || is_subclass_of($binding, 'Puli\\Repository\\Discovery\\ResourceBinding');\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getAcceptedBindingClass()\n    {\n        return 'Puli\\Repository\\Discovery\\ResourceBinding';\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function initializeBinding(Binding $binding)\n    {\n        Assert::isInstanceOf($binding, 'Puli\\Repository\\Discovery\\ResourceBinding', 'The binding must be an instance of ResourceBinding. Got: %s');\n\n        /* @var ResourceBinding $binding */\n        $binding->setRepository($this->repo);\n    }\n}\n"
  },
  {
    "path": "src/FilesystemRepository.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository;\n\nuse Iterator;\nuse Puli\\Repository\\Api\\ChangeStream\\ChangeStream;\nuse Puli\\Repository\\Api\\Resource\\BodyResource;\nuse Puli\\Repository\\Api\\Resource\\FilesystemResource;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\nuse Puli\\Repository\\Api\\ResourceCollection;\nuse Puli\\Repository\\Api\\ResourceNotFoundException;\nuse Puli\\Repository\\Api\\UnsupportedOperationException;\nuse Puli\\Repository\\Api\\UnsupportedResourceException;\nuse Puli\\Repository\\Resource\\Collection\\FilesystemResourceCollection;\nuse Puli\\Repository\\Resource\\DirectoryResource;\nuse Puli\\Repository\\Resource\\FileResource;\nuse Puli\\Repository\\Resource\\LinkResource;\nuse Symfony\\Component\\Filesystem\\Exception\\IOException;\nuse Symfony\\Component\\Filesystem\\Filesystem;\nuse Webmozart\\Assert\\Assert;\nuse Webmozart\\Glob\\Iterator\\GlobIterator;\nuse Webmozart\\Glob\\Iterator\\RecursiveDirectoryIterator;\nuse Webmozart\\PathUtil\\Path;\n\n/**\n * A repository reading from the file system.\n *\n * Resources can be read using their absolute file system paths:\n *\n * ```php\n * use Puli\\Repository\\FilesystemRepository;\n *\n * $repo = new FilesystemRepository();\n * $resource = $repo->get('/home/puli/.gitconfig');\n * ```\n *\n * The returned resources implement {@link FilesystemResource}.\n *\n * Optionally, a root directory can be passed to the constructor. Then all paths\n * will be read relative to that directory:\n *\n * ```php\n * $repo = new FilesystemRepository('/home/puli');\n * $resource = $repo->get('/.gitconfig');\n * ```\n *\n * While \".\" and \"..\" segments are supported, files outside the root directory\n * cannot be read. Any leading \"..\" segments will simply be stripped off.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass FilesystemRepository extends AbstractEditableRepository\n{\n    /**\n     * @var bool|null\n     */\n    private static $symlinkSupported;\n\n    /**\n     * @var string\n     */\n    private $baseDir;\n\n    /**\n     * @var int\n     */\n    private $baseDirLength;\n\n    /**\n     * @var bool\n     */\n    private $symlink;\n\n    /**\n     * @var bool\n     */\n    private $relative;\n\n    /**\n     * @var Filesystem\n     */\n    private $filesystem;\n\n    /**\n     * Returns whether symlinks are supported in the local environment.\n     *\n     * @return bool Returns `true` if symlinks are supported.\n     */\n    public static function isSymlinkSupported()\n    {\n        if (null === self::$symlinkSupported) {\n            // http://php.net/manual/en/function.symlink.php\n            // Symlinks are only supported on Windows Vista, Server 2008 or\n            // greater on PHP 5.3+\n            if (defined('PHP_WINDOWS_VERSION_MAJOR')) {\n                self::$symlinkSupported = PHP_WINDOWS_VERSION_MAJOR >= 6;\n            } else {\n                self::$symlinkSupported = true;\n            }\n        }\n\n        return self::$symlinkSupported;\n    }\n\n    /**\n     * Creates a new repository.\n     *\n     * @param string            $baseDir      The base directory of the repository on the file\n     *                                        system.\n     * @param bool              $symlink      Whether to use symbolic links for added files. If\n     *                                        symbolic links are not supported on the current\n     *                                        system, the repository will create hard copies\n     *                                        instead.\n     * @param bool              $relative     Whether to create relative symbolic links. If\n     *                                        relative links are not supported on the current\n     *                                        system, the repository will create absolute links\n     *                                        instead.\n     * @param ChangeStream|null $changeStream If provided, the repository will log\n     *                                        resources changes in this change stream.\n     */\n    public function __construct($baseDir = '/', $symlink = true, $relative = true, ChangeStream $changeStream = null)\n    {\n        parent::__construct($changeStream);\n\n        Assert::directory($baseDir);\n        Assert::boolean($symlink);\n\n        $this->baseDir = rtrim(Path::canonicalize($baseDir), '/');\n        $this->baseDirLength = strlen($baseDir);\n        $this->symlink = $symlink && self::isSymlinkSupported();\n        $this->relative = $this->symlink && $relative;\n        $this->filesystem = new Filesystem();\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function get($path)\n    {\n        $path = $this->sanitizePath($path);\n        $filesystemPath = $this->baseDir.$path;\n\n        if (!file_exists($filesystemPath)) {\n            throw ResourceNotFoundException::forPath($path);\n        }\n\n        return $this->createResource($filesystemPath, $path);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function find($query, $language = 'glob')\n    {\n        return $this->iteratorToCollection($this->getGlobIterator($query, $language));\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function contains($query, $language = 'glob')\n    {\n        $iterator = $this->getGlobIterator($query, $language);\n        $iterator->rewind();\n\n        return $iterator->valid();\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function hasChildren($path)\n    {\n        $filesystemPath = $this->getFilesystemPath($path);\n\n        if (!is_dir($filesystemPath)) {\n            return false;\n        }\n\n        $iterator = $this->getDirectoryIterator($filesystemPath);\n        $iterator->rewind();\n\n        return $iterator->valid();\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function listChildren($path)\n    {\n        $filesystemPath = $this->getFilesystemPath($path);\n\n        if (!is_dir($filesystemPath)) {\n            return new FilesystemResourceCollection();\n        }\n\n        return $this->iteratorToCollection($this->getDirectoryIterator($filesystemPath));\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function add($path, $resource)\n    {\n        $path = $this->sanitizePath($path);\n\n        if ($resource instanceof ResourceCollection) {\n            $this->ensureDirectoryExists($path);\n            foreach ($resource as $child) {\n                $this->addResource($path.'/'.$child->getName(), $child);\n            }\n\n            return;\n        }\n\n        if ($resource instanceof PuliResource) {\n            $this->ensureDirectoryExists(Path::getDirectory($path));\n            $this->addResource($path, $resource);\n\n            return;\n        }\n\n        throw new UnsupportedResourceException(sprintf(\n            'The passed resource must be a PuliResource or ResourceCollection. Got: %s',\n            is_object($resource) ? get_class($resource) : gettype($resource)\n        ));\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function remove($query, $language = 'glob')\n    {\n        $iterator = $this->getGlobIterator($query, $language);\n        $removed = 0;\n\n        Assert::notEmpty(trim($query, '/'), 'The root directory cannot be removed.');\n\n        // There's some problem with concurrent deletions at the moment\n        foreach (iterator_to_array($iterator) as $filesystemPath) {\n            $this->removeResource($filesystemPath, $removed);\n        }\n\n        return $removed;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function clear()\n    {\n        $iterator = $this->getDirectoryIterator($this->baseDir);\n        $removed = 0;\n\n        // Batch-delete all versions\n        $this->clearVersions();\n\n        foreach ($iterator as $filesystemPath) {\n            $this->removeResource($filesystemPath, $removed);\n        }\n\n        $this->storeVersion($this->get('/'));\n\n        return $removed;\n    }\n\n    private function ensureDirectoryExists($path)\n    {\n        $filesystemPath = $this->baseDir.$path;\n\n        if (is_file($filesystemPath)) {\n            throw new UnsupportedOperationException(sprintf(\n                'Instances of BodyResource do not support child resources in '.\n                'FilesystemRepository. Tried to add a child to %s.',\n                $filesystemPath\n            ));\n        }\n\n        if (!is_dir($filesystemPath)) {\n            mkdir($filesystemPath, 0777, true);\n        }\n    }\n\n    private function addResource($path, PuliResource $resource, $checkParentsForSymlinks = true)\n    {\n        $pathInBaseDir = $this->baseDir.$path;\n        $hasChildren = $resource->hasChildren();\n        $hasBody = $resource instanceof BodyResource;\n\n        if ($hasChildren && $hasBody) {\n            throw new UnsupportedResourceException(sprintf(\n                'Instances of BodyResource do not support child resources in '.\n                'FilesystemRepository. Tried to add a BodyResource with '.\n                'children at %s.',\n                $path\n            ));\n        }\n\n        $resource = clone $resource;\n        $resource->attachTo($this, $path);\n\n        if ($this->symlink && $checkParentsForSymlinks) {\n            $this->replaceParentSymlinksByCopies($path);\n        }\n\n        if ($resource instanceof FilesystemResource) {\n            if ($this->symlink) {\n                $this->symlinkMirror($resource->getFilesystemPath(), $pathInBaseDir);\n            } elseif ($hasBody) {\n                $this->filesystem->copy($resource->getFilesystemPath(), $pathInBaseDir);\n            } else {\n                $this->filesystem->mirror($resource->getFilesystemPath(), $pathInBaseDir);\n            }\n\n            $this->storeVersion($resource);\n\n            return;\n        }\n\n        if ($resource instanceof LinkResource) {\n            if (!$this->symlink) {\n                throw new UnsupportedResourceException(sprintf(\n                    'LinkResource requires support of symbolic links in FilesystemRepository. '.\n                    'Tried to add a LinkResource at %s.',\n                    $path\n                ));\n            }\n\n            $this->filesystem->symlink($this->baseDir.$resource->getTargetPath(), $pathInBaseDir);\n\n            $this->storeVersion($resource);\n\n            return;\n        }\n\n        if ($hasBody) {\n            file_put_contents($pathInBaseDir, $resource->getBody());\n\n            $this->storeVersion($resource);\n\n            return;\n        }\n\n        if (is_file($pathInBaseDir)) {\n            $this->filesystem->remove($pathInBaseDir);\n        }\n\n        if (!file_exists($pathInBaseDir)) {\n            mkdir($pathInBaseDir, 0777, true);\n        }\n\n        foreach ($resource->listChildren() as $child) {\n            $this->addResource($path.'/'.$child->getName(), $child, false);\n        }\n\n        $this->storeVersion($resource);\n    }\n\n    private function removeResource($filesystemPath, &$removed)\n    {\n        // Skip paths that have already been removed\n        if (!file_exists($filesystemPath)) {\n            return;\n        }\n\n        $this->removeVersions($this->getPath($filesystemPath));\n\n        ++$removed;\n\n        if (is_dir($filesystemPath) && !is_link($filesystemPath)) {\n            $iterator = $this->getDirectoryIterator($filesystemPath);\n\n            foreach ($iterator as $childFilesystemPath) {\n                // Remove children and child versions\n                $this->removeResource($childFilesystemPath, $removed);\n            }\n        }\n\n        $this->filesystem->remove($filesystemPath);\n    }\n\n    private function createResource($filesystemPath, $path)\n    {\n        $resource = null;\n\n        if (is_link($filesystemPath)) {\n            $baseDir = rtrim($this->baseDir, '/');\n            $targetFilesystemPath = $this->readLink($filesystemPath);\n\n            if (Path::isBasePath($baseDir, $targetFilesystemPath)) {\n                $targetPath = '/'.Path::makeRelative($targetFilesystemPath, $baseDir);\n                $resource = new LinkResource($targetPath);\n            }\n        }\n\n        if (!$resource && is_dir($filesystemPath)) {\n            $resource = new DirectoryResource($filesystemPath);\n        }\n\n        if (!$resource) {\n            $resource = new FileResource($filesystemPath);\n        }\n\n        $resource->attachTo($this, $path);\n\n        return $resource;\n    }\n\n    private function iteratorToCollection(Iterator $iterator)\n    {\n        $filesystemPaths = iterator_to_array($iterator);\n        $resources = array();\n\n        // RecursiveDirectoryIterator is not guaranteed to return sorted results\n        sort($filesystemPaths);\n\n        foreach ($filesystemPaths as $filesystemPath) {\n            $resource = is_dir($filesystemPath)\n                ? new DirectoryResource($filesystemPath, $this->getPath($filesystemPath))\n                : new FileResource($filesystemPath, $this->getPath($filesystemPath));\n\n            $resource->attachTo($this);\n\n            $resources[] = $resource;\n        }\n\n        return new FilesystemResourceCollection($resources);\n    }\n\n    private function getFilesystemPath($path)\n    {\n        $path = $this->sanitizePath($path);\n        $filesystemPath = $this->baseDir.$path;\n\n        if (!file_exists($filesystemPath)) {\n            throw ResourceNotFoundException::forPath($path);\n        }\n\n        return $filesystemPath;\n    }\n\n    private function getGlobIterator($query, $language)\n    {\n        $this->failUnlessGlob($language);\n\n        Assert::stringNotEmpty($query, 'The glob must be a non-empty string. Got: %s');\n        Assert::startsWith($query, '/', 'The glob %s is not absolute.');\n\n        $query = Path::canonicalize($query);\n\n        return new GlobIterator($this->baseDir.$query);\n    }\n\n    private function getDirectoryIterator($filesystemPath)\n    {\n        return new RecursiveDirectoryIterator(\n            $filesystemPath,\n            RecursiveDirectoryIterator::CURRENT_AS_PATHNAME | RecursiveDirectoryIterator::SKIP_DOTS\n        );\n    }\n\n    private function symlinkMirror($origin, $target, array $dirsToKeep = array())\n    {\n        $targetIsDir = is_dir($target);\n        $forceDir = in_array($target, $dirsToKeep, true);\n\n        // Merge directories\n        if (is_dir($origin) && ($targetIsDir || $forceDir)) {\n            if (is_link($target)) {\n                $this->replaceLinkByCopy($target, $dirsToKeep);\n            }\n\n            $iterator = $this->getDirectoryIterator($origin);\n\n            foreach ($iterator as $path) {\n                $this->symlinkMirror($path, $target.'/'.basename($path), $dirsToKeep);\n            }\n\n            return;\n        }\n\n        // Replace target\n        if (file_exists($target)) {\n            $this->filesystem->remove($target);\n        }\n\n        // Try creating a relative link\n        if ($this->relative && $this->trySymlink(Path::makeRelative($origin, Path::getDirectory($target)), $target)) {\n            return;\n        }\n\n        // Try creating a absolute link\n        if ($this->trySymlink($origin, $target)) {\n            return;\n        }\n\n        // Fall back to copy\n        if (is_dir($origin)) {\n            $this->filesystem->mirror($origin, $target);\n\n            return;\n        }\n\n        $this->filesystem->copy($origin, $target);\n    }\n\n    private function replaceParentSymlinksByCopies($path)\n    {\n        $previousPath = null;\n\n        // Collect all paths that MUST NOT be symlinks after doing the\n        // replace operation.\n        //\n        // Example:\n        //\n        // $dirsToKeep = ['/path/to/webmozart', '/path/to/webmozart/views']\n        //\n        // Before:\n        //   /webmozart -> target\n        //\n        // After:\n        //   /webmozart\n        //     /config -> target/config\n        //     /views\n        //       /index.html.twig -> target/views/index.html.twig\n\n        $dirsToKeep = array();\n\n        while ($previousPath !== ($path = Path::getDirectory($path))) {\n            $filesystemPath = $this->baseDir.$path;\n            $dirsToKeep[] = $filesystemPath;\n\n            if (is_link($filesystemPath)) {\n                $this->replaceLinkByCopy($filesystemPath, $dirsToKeep);\n\n                return;\n            }\n\n            $previousPath = $path;\n        }\n    }\n\n    private function replaceLinkByCopy($path, array $dirsToKeep = array())\n    {\n        $target = Path::makeAbsolute($this->readLink($path), Path::getDirectory($path));\n        $this->filesystem->remove($path);\n        $this->filesystem->mkdir($path);\n        $this->symlinkMirror($target, $path, $dirsToKeep);\n    }\n\n    private function trySymlink($origin, $target)\n    {\n        try {\n            $this->filesystem->symlink($origin, $target);\n\n            if (file_exists($target)) {\n                return true;\n            }\n        } catch (IOException $e) {\n        }\n\n        return false;\n    }\n\n    private function readLink($filesystemPath)\n    {\n        // On Windows, transitive links are resolved to the final target by\n        // readlink(). realpath(), however, returns the target link on Windows,\n        // but not on Unix.\n\n        // /link1 -> /link2 -> /file\n\n        // Windows: readlink(/link1) => /file\n        //          realpath(/link1) => /link2\n\n        // Unix:    readlink(/link1) => /link2\n        //          realpath(/link1) => /file\n\n        // Consistency FTW!\n\n        return '\\\\' === DIRECTORY_SEPARATOR ? realpath($filesystemPath) : readlink($filesystemPath);\n    }\n\n    private function getPath($filesystemPath)\n    {\n        return substr($filesystemPath, $this->baseDirLength);\n    }\n}\n"
  },
  {
    "path": "src/InMemoryRepository.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository;\n\nuse ArrayIterator;\nuse Puli\\Repository\\Api\\ChangeStream\\ChangeStream;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\nuse Puli\\Repository\\Api\\ResourceCollection;\nuse Puli\\Repository\\Api\\ResourceNotFoundException;\nuse Puli\\Repository\\Api\\UnsupportedResourceException;\nuse Puli\\Repository\\Resource\\Collection\\ArrayResourceCollection;\nuse Puli\\Repository\\Resource\\GenericResource;\nuse Webmozart\\Assert\\Assert;\nuse Webmozart\\Glob\\Glob;\nuse Webmozart\\Glob\\Iterator\\GlobFilterIterator;\nuse Webmozart\\Glob\\Iterator\\RegexFilterIterator;\nuse Webmozart\\PathUtil\\Path;\n\n/**\n * An in-memory resource repository.\n *\n * Resources can be added with the method {@link add()}:\n *\n * ```php\n * use Puli\\Repository\\InMemoryRepository;\n *\n * $repo = new InMemoryRepository();\n * $repo->add('/css', new DirectoryResource('/path/to/project/res/css'));\n * ```\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass InMemoryRepository extends AbstractEditableRepository\n{\n    /**\n     * @var PuliResource[]\n     */\n    private $resources = array();\n\n    /**\n     * Create the repository.\n     *\n     * @param ChangeStream|null $changeStream If provided, the repository will log\n     *                                        resources changes in this change stream.\n     */\n    public function __construct(ChangeStream $changeStream = null)\n    {\n        parent::__construct($changeStream);\n\n        $this->clear();\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function get($path)\n    {\n        $path = $this->sanitizePath($path);\n\n        if (!isset($this->resources[$path])) {\n            throw ResourceNotFoundException::forPath($path);\n        }\n\n        return $this->resources[$path];\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function find($query, $language = 'glob')\n    {\n        $this->failUnlessGlob($language);\n\n        $query = $this->sanitizePath($query);\n        $resources = array();\n\n        if (Glob::isDynamic($query)) {\n            $resources = $this->getGlobIterator($query);\n        } elseif (isset($this->resources[$query])) {\n            $resources = array($this->resources[$query]);\n        }\n\n        return new ArrayResourceCollection($resources);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function contains($query, $language = 'glob')\n    {\n        $this->failUnlessGlob($language);\n\n        $query = $this->sanitizePath($query);\n\n        if (Glob::isDynamic($query)) {\n            $iterator = $this->getGlobIterator($query);\n            $iterator->rewind();\n\n            return $iterator->valid();\n        }\n\n        return isset($this->resources[$query]);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function add($path, $resource)\n    {\n        $path = $this->sanitizePath($path);\n\n        if ($resource instanceof ResourceCollection) {\n            $this->ensureDirectoryExists($path);\n            foreach ($resource as $child) {\n                $this->addResource($path.'/'.$child->getName(), $child);\n            }\n\n            // Keep the resources sorted by file name\n            ksort($this->resources);\n\n            return;\n        }\n\n        if ($resource instanceof PuliResource) {\n            $this->ensureDirectoryExists(Path::getDirectory($path));\n            $this->addResource($path, $resource);\n\n            ksort($this->resources);\n\n            return;\n        }\n\n        throw new UnsupportedResourceException(sprintf(\n            'The passed resource must be a PuliResource or ResourceCollection. Got: %s',\n            is_object($resource) ? get_class($resource) : gettype($resource)\n        ));\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function remove($query, $language = 'glob')\n    {\n        $resources = $this->find($query, $language);\n        $nbOfResources = count($this->resources);\n\n        // Run the assertion after find(), so that we know that $query is valid\n        Assert::notEmpty(trim($query, '/'), 'The root directory cannot be removed.');\n\n        foreach ($resources as $resource) {\n            $this->removeResource($resource);\n        }\n\n        return $nbOfResources - count($this->resources);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function clear()\n    {\n        $root = new GenericResource('/');\n        $root->attachTo($this);\n\n        // Subtract root\n        $removed = count($this->resources) - 1;\n\n        $this->resources = array('/' => $root);\n\n        $this->clearVersions();\n        $this->storeVersion($root);\n\n        return $removed;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function listChildren($path)\n    {\n        $iterator = $this->getChildIterator($this->get($path));\n\n        return new ArrayResourceCollection($iterator);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function hasChildren($path)\n    {\n        $iterator = $this->getChildIterator($this->get($path));\n        $iterator->rewind();\n\n        return $iterator->valid();\n    }\n\n    /**\n     * Recursively creates a directory for a path.\n     *\n     * @param string $path A directory path.\n     */\n    private function ensureDirectoryExists($path)\n    {\n        if (!isset($this->resources[$path])) {\n            // Recursively initialize parent directories\n            if ($path !== '/') {\n                $this->ensureDirectoryExists(Path::getDirectory($path));\n            }\n\n            $this->resources[$path] = new GenericResource($path);\n            $this->resources[$path]->attachTo($this);\n\n            return;\n        }\n    }\n\n    private function addResource($path, PuliResource $resource)\n    {\n        $basePath = '/' === $path ? $path : $path.'/';\n\n        // Read children before attaching the resource to this repository\n        $children = $resource->listChildren();\n\n        $resource = clone $resource;\n        $resource->attachTo($this, $path);\n\n        // Add the resource before adding its children, so that the array\n        // stays sorted\n        $this->resources[$path] = $resource;\n\n        foreach ($children as $name => $child) {\n            $this->addResource($basePath.$name, $child);\n        }\n\n        $this->storeVersion($resource);\n    }\n\n    private function removeResource(PuliResource $resource)\n    {\n        $path = $resource->getPath();\n\n        $this->removeVersions($path);\n\n        // Ignore non-existing resources\n        if (!isset($this->resources[$path])) {\n            return;\n        }\n\n        // Recursively remove directory contents\n        foreach ($this->getChildIterator($resource) as $child) {\n            $this->removeResource($child);\n        }\n\n        unset($this->resources[$path]);\n\n        // Detach from locator\n        $resource->detach();\n    }\n\n    /**\n     * Returns an iterator for the children of a resource.\n     *\n     * @param PuliResource $resource The resource.\n     *\n     * @return RegexFilterIterator The iterator.\n     */\n    private function getChildIterator(PuliResource $resource)\n    {\n        $staticPrefix = rtrim($resource->getPath(), '/').'/';\n        $regExp = '~^'.preg_quote($staticPrefix, '~').'[^/]+$~';\n\n        return new RegexFilterIterator(\n            $regExp,\n            $staticPrefix,\n            new ArrayIterator($this->resources),\n            RegexFilterIterator::FILTER_KEY\n        );\n    }\n\n    /**\n     * Returns an iterator for a glob.\n     *\n     * @param string $glob The glob.\n     *\n     * @return GlobFilterIterator The iterator.\n     */\n    protected function getGlobIterator($glob)\n    {\n        return new GlobFilterIterator(\n            $glob,\n            new ArrayIterator($this->resources),\n            GlobFilterIterator::FILTER_KEY\n        );\n    }\n}\n"
  },
  {
    "path": "src/JsonRepository.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository;\n\nuse InvalidArgumentException;\nuse Puli\\Repository\\Api\\ChangeStream\\VersionList;\nuse Puli\\Repository\\Api\\EditableRepository;\nuse Puli\\Repository\\Api\\NoVersionFoundException;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\nuse RecursiveIteratorIterator;\nuse Webmozart\\Glob\\Glob;\nuse Webmozart\\Glob\\Iterator\\RecursiveDirectoryIterator;\nuse Webmozart\\PathUtil\\Path;\n\n/**\n * A repository backed by a JSON file optimized for development.\n *\n * The generated JSON file is described by res/schema/repository-schema-1.0.json.\n *\n * Resources can be added with the method {@link add()}:\n *\n * ```php\n * use Puli\\Repository\\JsonRepository;\n *\n * $repo = new JsonRepository('/path/to/repository.json', '/path/to/project');\n * $repo->add('/css', new DirectoryResource('/path/to/project/res/css'));\n * ```\n *\n * When adding a resource, the added filesystem path is stored in the JSON file\n * under the key of the Puli path. The path is stored relatively to the base\n * directory passed to the constructor:\n *\n * ```json\n * {\n *     \"/css\": \"res/css\"\n * }\n * ```\n *\n * Mapped resources can be read with the method {@link get()}:\n *\n * ```php\n * $cssPath = $repo->get('/css')->getFilesystemPath();\n * ```\n *\n * You can also access nested files:\n *\n * ```php\n * echo $repo->get('/css/style.css')->getBody();\n * ```\n *\n * Nested files are searched during {@link get()}. As a consequence, this\n * implementation should not be used in production environments. Use\n * {@link OptimizedJsonRepository} instead which searches nested files during\n * {@link add()} and has much faster read performance.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n * @author Titouan Galopin <galopintitouan@gmail.com>\n */\nclass JsonRepository extends AbstractJsonRepository implements EditableRepository\n{\n    /**\n     * Flag: Don't search the contents of mapped directories for matching paths.\n     *\n     * @internal\n     */\n    const NO_SEARCH_FILESYSTEM = 2;\n\n    /**\n     * Flag: Don't filter out references that don't exist on the filesystem.\n     *\n     * @internal\n     */\n    const NO_CHECK_FILE_EXISTS = 4;\n\n    /**\n     * Flag: Include the references for mapped ancestor paths /a of a path /a/b.\n     *\n     * @internal\n     */\n    const INCLUDE_ANCESTORS = 8;\n\n    /**\n     * Flag: Include the references for mapped nested paths /a/b of a path /a.\n     *\n     * @internal\n     */\n    const INCLUDE_NESTED = 16;\n\n    /**\n     * Creates a new repository.\n     *\n     * @param string $path          The path to the JSON file. If relative, it\n     *                              must be relative to the base directory.\n     * @param string $baseDirectory The base directory of the store. Paths\n     *                              inside that directory are stored as relative\n     *                              paths. Paths outside that directory are\n     *                              stored as absolute paths.\n     * @param bool   $validateJson  Whether to validate the JSON file against\n     *                              the schema. Slow but spots problems.\n     */\n    public function __construct($path, $baseDirectory, $validateJson = false)\n    {\n        // Does not accept ChangeStream objects\n        // The ChangeStream functionality is implemented by the repository itself\n        parent::__construct($path, $baseDirectory, $validateJson);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getVersions($path)\n    {\n        if (!$this->json) {\n            $this->load();\n        }\n\n        $references = $this->searchReferences($path);\n\n        if (!isset($references[$path])) {\n            throw NoVersionFoundException::forPath($path);\n        }\n\n        $resources = array();\n        $pathReferences = $references[$path];\n\n        // The first reference is the last (current) version\n        // Hence traverse in reverse order\n        for ($ref = end($pathReferences); null !== key($pathReferences); $ref = prev($pathReferences)) {\n            $resources[] = $this->createResource($path, $ref);\n        }\n\n        return new VersionList($path, $resources);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    protected function storeVersion(PuliResource $resource)\n    {\n        $path = $resource->getPath();\n\n        // Newly inserted parent directories and the resource need to be\n        // sorted before we can correctly search references below\n        krsort($this->json);\n\n        // If a mapping exists for a sub-path of this resource\n        // (e.g. $path = /a, mapped sub-path = /a/b)\n        // we need to record the order, since by default sub-paths are\n        // preferred over super paths\n\n        $references = $this->searchReferences(\n            $path,\n            // Don't do filesystem checks here. We only check the filesystem\n            // when reading, not when adding.\n            self::NO_SEARCH_FILESYSTEM | self::NO_CHECK_FILE_EXISTS\n                // Include references for mapped ancestor and nested paths\n                | self::INCLUDE_ANCESTORS | self::INCLUDE_NESTED\n        );\n\n        // Filter virtual resources\n        $references = array_filter($references, function ($currentReferences) {\n            return array(null) !== $currentReferences;\n        });\n\n        // The $references contain:\n        // - any sub references (e.g. /a/b/c, /a/b/d)\n        // - the reference itself at $pos (e.g. /a/b)\n        // - non-null parent references (e.g. /a)\n        // (in that order, since long paths are sorted before short paths)\n        $pos = array_search($path, array_keys($references), true);\n\n        // We need to do three things:\n\n        // 1. If any parent mapping has an order defined, inherit that order\n\n        if ($pos + 1 < count($references)) {\n            // Inherit the parent order if necessary\n            if (!isset($this->json['_order'][$path])) {\n                $parentReferences = array_slice($references, $pos + 1);\n\n                $this->initWithParentOrder($path, $parentReferences);\n            }\n\n            // A parent order was inherited. Insert the path itself.\n            if (isset($this->json['_order'][$path])) {\n                $this->prependOrderEntry($path, $path);\n            }\n        }\n\n        // 2. If there are child mappings, insert the current path into their order\n\n        if ($pos > 0) {\n            $subReferences = array_slice($references, 0, $pos);\n\n            foreach ($subReferences as $subPath => $_) {\n                if (isset($this->json['_order'][$subPath])) {\n                    continue;\n                }\n\n                if (isset($this->json['_order'][$path])) {\n                    $this->json['_order'][$subPath] = $this->json['_order'][$path];\n                } else {\n                    $this->initWithDefaultOrder($subPath, $path, $references);\n                }\n            }\n\n            // After initializing all order entries, insert the new one\n            foreach ($subReferences as $subPath => $_) {\n                $this->prependOrderEntry($subPath, $path);\n            }\n        }\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    protected function insertReference($path, $reference)\n    {\n        if (!isset($this->json[$path])) {\n            // Store first entries as simple reference\n            $this->json[$path] = $reference;\n\n            return;\n        }\n\n        if ($reference === $this->json[$path]) {\n            // Reference is already set\n            return;\n        }\n\n        if (!is_array($this->json[$path])) {\n            // Convert existing entries to arrays for follow ups\n            $this->json[$path] = array($this->json[$path]);\n        }\n\n        if (!in_array($reference, $this->json[$path], true)) {\n            // Insert at the beginning of the array\n            array_unshift($this->json[$path], $reference);\n        }\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    protected function removeReferences($glob)\n    {\n        $checkResults = $this->getReferencesForGlob($glob);\n        $nonDeletablePaths = array();\n\n        foreach ($checkResults as $path => $filesystemPath) {\n            if (!array_key_exists($path, $this->json)) {\n                $nonDeletablePaths[] = $filesystemPath;\n            }\n        }\n\n        if (count($nonDeletablePaths) > 0) {\n            throw new InvalidArgumentException(sprintf(\n                'You cannot remove resources that are not mapped in the JSON '.\n                'file. Tried to remove %s%s.',\n                reset($nonDeletablePaths),\n                count($nonDeletablePaths) > 1\n                    ? ' and '.(count($nonDeletablePaths) - 1).' more'\n                    : ''\n            ));\n        }\n\n        $deletedPaths = $this->getReferencesForGlob($glob.'{,/**/*}', self::NO_SEARCH_FILESYSTEM);\n        $removed = 0;\n\n        foreach ($deletedPaths as $path => $filesystemPath) {\n            $removed += 1 + count($this->getReferencesForGlob($path.'/**/*'));\n\n            unset($this->json[$path]);\n        }\n\n        return $removed;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    protected function getReferencesForPath($path)\n    {\n        // Stop on first result and flatten\n        return $this->flatten($this->searchReferences($path, self::STOP_ON_FIRST));\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    protected function getReferencesForGlob($glob, $flags = 0)\n    {\n        if (!Glob::isDynamic($glob)) {\n            return $this->getReferencesForPath($glob);\n        }\n\n        return $this->getReferencesForRegex(\n            Glob::getBasePath($glob),\n            Glob::toRegEx($glob),\n            $flags\n        );\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    protected function getReferencesForRegex($staticPrefix, $regex, $flags = 0, $maxDepth = 0)\n    {\n        return $this->flattenWithFilter(\n            // Never stop on the first result before applying the filter since\n            // the filter may reject the only returned path\n            $this->searchReferences($staticPrefix, self::INCLUDE_NESTED),\n            $regex,\n            $flags,\n            $maxDepth\n        );\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    protected function getReferencesInDirectory($path, $flags = 0)\n    {\n        $basePath = rtrim($path, '/');\n\n        return $this->getReferencesForRegex(\n            $basePath.'/',\n            '~^'.preg_quote($basePath, '~').'/[^/]+$~',\n            $flags,\n            // Limit the directory exploration to the depth of the path + 1\n            $this->getPathDepth($path) + 1\n        );\n    }\n\n    /**\n     * Flattens a two-level reference array into a one-level array.\n     *\n     * For each entry on the first level, only the first entry of the second\n     * level is included in the result.\n     *\n     * Each reference returned by this method can be:\n     *\n     *  * `null`\n     *  * a link starting with `@`\n     *  * an absolute filesystem path\n     *\n     * The keys of the returned array are Puli paths. Their order is undefined.\n     *\n     * @param array $references A two-level reference array as returned by\n     *                          {@link searchReferences()}.\n     *\n     * @return string[]|null[] A one-level array of references with Puli paths\n     *                         as keys.\n     */\n    private function flatten(array $references)\n    {\n        $result = array();\n\n        foreach ($references as $currentPath => $currentReferences) {\n            if (!isset($result[$currentPath])) {\n                $result[$currentPath] = reset($currentReferences);\n            }\n        }\n\n        return $result;\n    }\n\n    /**\n     * Flattens a two-level reference array into a one-level array and filters\n     * out any references that don't match the given regular expression.\n     *\n     * This method takes a two-level reference array as returned by\n     * {@link searchReferences()}. The references are scanned for Puli paths\n     * matching the given regular expression. Those matches are returned.\n     *\n     * If a matching path refers to more than one reference, the first reference\n     * is returned in the resulting array.\n     *\n     * All references that contain directory paths may be traversed recursively and\n     * scanned for more paths matching the regular expression. This recursive\n     * traversal can be limited by passing a `$maxDepth` (see {@link getPathDepth()}).\n     * By default, this `$maxDepth` is equal to zero (no recursive scan).\n     *\n     * The flag `STOP_ON_FIRST` may be used to stop the search at the first result.\n     *\n     * The flag `NO_SEARCH_FILESYSTEM` may be used to check for whether the found\n     * paths actually exist on the filesystem.\n     *\n     * Each reference returned by this method can be:\n     *\n     *  * `null`\n     *  * a link starting with `@`\n     *  * an absolute filesystem path\n     *\n     * The keys of the returned array are Puli paths. Their order is undefined.\n     *\n     * @param array  $references A two-level reference array as returned by\n     *                           {@link searchReferences()}.\n     * @param string $regex      A regular expression used to filter Puli paths.\n     * @param int    $flags      A bitwise combination of the flag constants in\n     *                           this class.\n     * @param int    $maxDepth   The maximum path depth when searching the\n     *                           contents of directory references. If 0, the\n     *                           depth is unlimited.\n     *\n     * @return string[]|null[] A one-level array of references with Puli paths\n     *                         as keys.\n     */\n    private function flattenWithFilter(array $references, $regex, $flags = 0, $maxDepth = 0)\n    {\n        $result = array();\n\n        foreach ($references as $currentPath => $currentReferences) {\n            // Check whether the current entry matches the pattern\n            if (!isset($result[$currentPath]) && preg_match($regex, $currentPath)) {\n                // If yes, the first stored reference is returned\n                $result[$currentPath] = reset($currentReferences);\n\n                if ($flags & self::STOP_ON_FIRST) {\n                    return $result;\n                }\n            }\n\n            if ($flags & self::NO_SEARCH_FILESYSTEM) {\n                continue;\n            }\n\n            // First follow any links before we check which of them is a directory\n            $currentReferences = $this->followLinks($currentReferences);\n            $currentPath = rtrim($currentPath, '/');\n\n            // Search the nested entries if desired\n            foreach ($currentReferences as $baseFilesystemPath) {\n                // Ignore null values and file paths\n                if (!is_dir($baseFilesystemPath)) {\n                    continue;\n                }\n\n                $iterator = new RecursiveIteratorIterator(\n                    new RecursiveDirectoryIterator(\n                        $baseFilesystemPath,\n                        RecursiveDirectoryIterator::CURRENT_AS_PATHNAME\n                            | RecursiveDirectoryIterator::SKIP_DOTS\n                    ),\n                    RecursiveIteratorIterator::SELF_FIRST\n                );\n\n                if (0 !== $maxDepth) {\n                    $currentDepth = $this->getPathDepth($currentPath);\n                    $maxIteratorDepth = $maxDepth - $currentDepth;\n\n                    if ($maxIteratorDepth < 1) {\n                        continue;\n                    }\n\n                    $iterator->setMaxDepth($maxIteratorDepth);\n                }\n\n                $basePathLength = strlen($baseFilesystemPath);\n\n                foreach ($iterator as $nestedFilesystemPath) {\n                    $nestedPath = substr_replace($nestedFilesystemPath, $currentPath, 0, $basePathLength);\n\n                    if (!isset($result[$nestedPath]) && preg_match($regex, $nestedPath)) {\n                        $result[$nestedPath] = $nestedFilesystemPath;\n\n                        if ($flags & self::STOP_ON_FIRST) {\n                            return $result;\n                        }\n                    }\n                }\n            }\n        }\n\n        return $result;\n    }\n\n    /**\n     * Filters the JSON file for all references relevant to a given search path.\n     *\n     * The JSON is scanned starting with the longest mapped Puli path.\n     *\n     * If the search path is \"/a/b\", the result includes:\n     *\n     *  * The references of the mapped path \"/a/b\".\n     *\n     * If the flag `INCLUDE_ANCESTORS` is used, the result additionally\n     * includes:\n     *\n     *  * The references of any mapped super path \"/a\" with the sub-path \"/b\"\n     *    appended.\n     *\n     * If the flag `INCLUDE_NESTED` is used, the result additionally\n     * includes:\n     *\n     *  * The references of any mapped sub path \"/a/b/c\".\n     *\n     * This is useful if you want to look for the children of \"/a/b\" or scan\n     * all descendants for paths matching a given pattern.\n     *\n     * The result of this method is an array with two levels:\n     *\n     *  * The first level has Puli paths as keys.\n     *  * The second level contains all references for that path, where the\n     *    first reference has the highest, the last reference the lowest\n     *    priority. The keys of the second level are integers. There may be\n     *    holes between any two keys.\n     *\n     * The references of the second level contain:\n     *\n     *  * `null` values for virtual resources\n     *  * strings starting with \"@\" for links\n     *  * absolute filesystem paths for filesystem resources\n     *\n     * The flag `STOP_ON_FIRST` may be used to stop the search at the first result.\n     *\n     * The flag `NO_SEARCH_FILESYSTEM` may be used to check for whether the found\n     * paths actually exist on the filesystem.\n     *\n     * @param string $searchPath The path to search.\n     * @param int    $flags      A bitwise combination of the flag constants in\n     *                           this class.\n     *\n     * @return array An array with two levels.\n     */\n    private function searchReferences($searchPath, $flags = 0)\n    {\n        $result = array();\n        $foundMatchingMappings = false;\n        $searchPath = rtrim($searchPath, '/');\n        $searchPathForTest = $searchPath.'/';\n\n        foreach ($this->json as $currentPath => $currentReferences) {\n            $currentPathForTest = rtrim($currentPath, '/').'/';\n\n            // We found a mapping that matches the search path\n            // e.g. mapping /a/b for path /a/b\n            if ($searchPathForTest === $currentPathForTest) {\n                $foundMatchingMappings = true;\n                $currentReferences = $this->resolveReferences($currentPath, $currentReferences, $flags);\n\n                if (empty($currentReferences)) {\n                    continue;\n                }\n\n                $result[$currentPath] = $currentReferences;\n\n                // Return unless an explicit mapping order is defined\n                // In that case, the ancestors need to be searched as well\n                if (($flags & self::STOP_ON_FIRST) && !isset($this->json['_order'][$currentPath])) {\n                    return $result;\n                }\n\n                continue;\n            }\n\n            // We found a mapping that lies within the search path\n            // e.g. mapping /a/b/c for path /a/b\n            if (($flags & self::INCLUDE_NESTED) && 0 === strpos($currentPathForTest, $searchPathForTest)) {\n                $foundMatchingMappings = true;\n                $currentReferences = $this->resolveReferences($currentPath, $currentReferences, $flags);\n\n                if (empty($currentReferences)) {\n                    continue;\n                }\n\n                $result[$currentPath] = $currentReferences;\n\n                // Return unless an explicit mapping order is defined\n                // In that case, the ancestors need to be searched as well\n                if (($flags & self::STOP_ON_FIRST) && !isset($this->json['_order'][$currentPath])) {\n                    return $result;\n                }\n\n                continue;\n            }\n\n            // We found a mapping that is an ancestor of the search path\n            // e.g. mapping /a for path /a/b\n            if (0 === strpos($searchPathForTest, $currentPathForTest)) {\n                $foundMatchingMappings = true;\n\n                if ($flags & self::INCLUDE_ANCESTORS) {\n                    // Include the references of the ancestor\n                    $currentReferences = $this->resolveReferences($currentPath, $currentReferences, $flags);\n\n                    if (empty($currentReferences)) {\n                        continue;\n                    }\n\n                    $result[$currentPath] = $currentReferences;\n\n                    // Return unless an explicit mapping order is defined\n                    // In that case, the ancestors need to be searched as well\n                    if (($flags & self::STOP_ON_FIRST) && !isset($this->json['_order'][$currentPath])) {\n                        return $result;\n                    }\n\n                    continue;\n                }\n\n                if ($flags & self::NO_SEARCH_FILESYSTEM) {\n                    continue;\n                }\n\n                // Check the filesystem directories pointed to by the ancestors\n                // for the searched path\n                $nestedPath = substr($searchPath, strlen($currentPathForTest));\n                $currentPathWithNested = rtrim($currentPath, '/').'/'.$nestedPath;\n\n                // Follow links so that we can check the nested directories in\n                // the final transitive link targets\n                $currentReferencesResolved = $this->followLinks(\n                    // Never stop on first, since appendNestedPath() might\n                    // discard the first but accept the second entry\n                    $this->resolveReferences($currentPath, $currentReferences, $flags & (~self::STOP_ON_FIRST))\n                );\n\n                // Append the path and check which of the resulting paths exist\n                $nestedReferences = $this->appendPathAndFilterExisting(\n                    $currentReferencesResolved,\n                    $nestedPath,\n                    $flags\n                );\n\n                // None of the results exists\n                if (empty($nestedReferences)) {\n                    continue;\n                }\n\n                // Return unless an explicit mapping order is defined\n                // In that case, the ancestors need to be searched as well\n                if (($flags & self::STOP_ON_FIRST) && !isset($this->json['_order'][$currentPathWithNested])) {\n                    // The nested references already have size 1\n                    return array($currentPathWithNested => $nestedReferences);\n                }\n\n                // We are traversing long keys before short keys\n                // It could be that this entry already exists.\n                if (!isset($result[$currentPathWithNested])) {\n                    $result[$currentPathWithNested] = $nestedReferences;\n\n                    continue;\n                }\n\n                // If no explicit mapping order is defined, simply append the\n                // new references to the existing ones\n                if (!isset($this->json['_order'][$currentPathWithNested])) {\n                    $result[$currentPathWithNested] = array_merge(\n                        $result[$currentPathWithNested],\n                        $nestedReferences\n                    );\n\n                    continue;\n                }\n\n                // If an explicit mapping order is defined, store the paths\n                // of the mappings that generated each reference set and\n                // resolve the order later on\n                if (!isset($result[$currentPathWithNested][$currentPathWithNested])) {\n                    $result[$currentPathWithNested] = array(\n                        $currentPathWithNested => $result[$currentPathWithNested],\n                    );\n                }\n\n                // Add the new references generated by the current mapping\n                $result[$currentPathWithNested][$currentPath] = $nestedReferences;\n\n                continue;\n            }\n\n            // We did not find anything but previously found mappings\n            // The mappings are sorted alphabetically, so we can safely abort\n            if ($foundMatchingMappings) {\n                break;\n            }\n        }\n\n        // Resolve the order where it is explicitly set\n        if (!isset($this->json['_order'])) {\n            return $result;\n        }\n\n        foreach ($result as $currentPath => $referencesByMappedPath) {\n            // If no order is defined for the path or if only one mapped path\n            // generated references, there's nothing to do\n            if (!isset($this->json['_order'][$currentPath]) || !isset($referencesByMappedPath[$currentPath])) {\n                continue;\n            }\n\n            $orderedReferences = array();\n\n            foreach ($this->json['_order'][$currentPath] as $orderEntry) {\n                if (!isset($referencesByMappedPath[$orderEntry['path']])) {\n                    continue;\n                }\n\n                for ($i = 0; $i < $orderEntry['references'] && count($referencesByMappedPath[$orderEntry['path']]) > 0; ++$i) {\n                    $orderedReferences[] = array_shift($referencesByMappedPath[$orderEntry['path']]);\n                }\n\n                // Only include references of the first mapped path\n                // Since $stopOnFirst is set, those references have a\n                // maximum size of 1\n                if ($flags & self::STOP_ON_FIRST) {\n                    break;\n                }\n            }\n\n            $result[$currentPath] = $orderedReferences;\n        }\n\n        return $result;\n    }\n\n    /**\n     * Follows any link in a list of references.\n     *\n     * This method takes all the given references, checks for links starting\n     * with \"@\" and recursively expands those links to their target references.\n     * The target references may be `null` or absolute filesystem paths.\n     *\n     * Null values are returned unchanged.\n     *\n     * Absolute filesystem paths are returned unchanged.\n     *\n     * The flag `STOP_ON_FIRST` may be used to stop the search at the first result.\n     *\n     * @param string[]|null[] $references The references.\n     * @param int             $flags      A bitwise combination of the flag\n     *                                    constants in this class.\n     *\n     * @return string[]|null[] The references with all links replaced by their\n     *                         target references. If any link pointed to more\n     *                         than one target reference, the returned array\n     *                         is larger than the passed array (unless the\n     *                         argument `$stopOnFirst` was set to `true`).\n     */\n    private function followLinks(array $references, $flags = 0)\n    {\n        $result = array();\n\n        foreach ($references as $key => $reference) {\n            // Not a link\n            if (!$this->isLinkReference($reference)) {\n                $result[] = $reference;\n\n                if ($flags & self::STOP_ON_FIRST) {\n                    return $result;\n                }\n\n                continue;\n            }\n\n            $referencedPath = substr($reference, 1);\n\n            // Get all the file system paths that this link points to\n            // and append them to the result\n            foreach ($this->searchReferences($referencedPath, $flags) as $referencedReferences) {\n                // Follow links recursively\n                $referencedReferences = $this->followLinks($referencedReferences);\n\n                // Append all resulting target paths to the result\n                foreach ($referencedReferences as $referencedReference) {\n                    $result[] = $referencedReference;\n\n                    if ($flags & self::STOP_ON_FIRST) {\n                        return $result;\n                    }\n                }\n            }\n        }\n\n        return $result;\n    }\n\n    /**\n     * Appends nested paths to references and filters out the existing ones.\n     *\n     * This method takes all the given references, appends the nested path to\n     * each of them and then filters out the results that actually exist on the\n     * filesystem.\n     *\n     * Null references are filtered out.\n     *\n     * Link references should be followed with {@link followLinks()} before\n     * calling this method.\n     *\n     * The flag `STOP_ON_FIRST` may be used to stop the search at the first result.\n     *\n     * @param string[]|null[] $references The references.\n     * @param string          $nestedPath The nested path to append without\n     *                                    leading slash (\"/\").\n     * @param int             $flags      A bitwise combination of the flag\n     *                                    constants in this class.\n     *\n     * @return string[] The references with the nested path appended. Each\n     *                  reference is guaranteed to exist on the filesystem.\n     */\n    private function appendPathAndFilterExisting(array $references, $nestedPath, $flags = 0)\n    {\n        $result = array();\n\n        foreach ($references as $reference) {\n            // Filter out null values\n            // Links should be followed before calling this method\n            if (null === $reference) {\n                continue;\n            }\n\n            $nestedReference = rtrim($reference, '/').'/'.$nestedPath;\n\n            if (file_exists($nestedReference)) {\n                $result[] = $nestedReference;\n\n                if ($flags & self::STOP_ON_FIRST) {\n                    return $result;\n                }\n            }\n        }\n\n        return $result;\n    }\n\n    /**\n     * Resolves a list of references stored in the JSON.\n     *\n     * Each reference passed in can be:\n     *\n     *  * `null`\n     *  * a link starting with `@`\n     *  * a filesystem path relative to the base directory\n     *  * an absolute filesystem path\n     *\n     * Each reference returned by this method can be:\n     *\n     *  * `null`\n     *  * a link starting with `@`\n     *  * an absolute filesystem path\n     *\n     * Additionally, the results are guaranteed to be an array.\n     *\n     * The flag `STOP_ON_FIRST` may be used to stop the search at the first result.\n     * In that case, the results array has a maximum size of 1.\n     *\n     * The flag `NO_SEARCH_FILESYSTEM` may be used to check for whether the found\n     * paths actually exist on the filesystem.\n     *\n     * @param string $path       The mapped Puli path.\n     * @param mixed  $references The reference(s).\n     * @param int    $flags      A bitwise combination of the flag constants in\n     *                           this class.\n     *\n     * @return string[]|null[] The resolved references.\n     */\n    private function resolveReferences($path, $references, $flags = 0)\n    {\n        $result = array();\n\n        if (!is_array($references)) {\n            $references = array($references);\n        }\n\n        foreach ($references as $key => $reference) {\n            // Keep non-filesystem references as they are\n            if (!$this->isFilesystemReference($reference)) {\n                $result[] = $reference;\n\n                if ($flags & self::STOP_ON_FIRST) {\n                    return $result;\n                }\n\n                continue;\n            }\n\n            $absoluteReference = Path::makeAbsolute($reference, $this->baseDirectory);\n            $referenceExists = file_exists($absoluteReference);\n\n            if (($flags & self::NO_CHECK_FILE_EXISTS) || $referenceExists) {\n                $result[] = $absoluteReference;\n\n                if ($flags & self::STOP_ON_FIRST) {\n                    return $result;\n                }\n            }\n\n            if (!$referenceExists) {\n                $this->logReferenceNotFound($path, $reference, $absoluteReference);\n            }\n        }\n\n        return $result;\n    }\n\n    /**\n     * Returns the depth of a Puli path.\n     *\n     * The depth is used in order to limit the recursion when recursively\n     * iterating directories.\n     *\n     * The depth starts at 0 for the root:\n     *\n     * /                0\n     * /webmozart       1\n     * /webmozart/puli  2\n     * ...\n     *\n     * @param string $path A Puli path.\n     *\n     * @return int The depth starting with 0 for the root node.\n     */\n    private function getPathDepth($path)\n    {\n        // / has depth 0\n        // /webmozart has depth 1\n        // /webmozart/puli has depth 2\n        // ...\n        return substr_count(rtrim($path, '/'), '/');\n    }\n\n    /**\n     * Inserts a path at the beginning of the order list of a mapped path.\n     *\n     * @param string $path          The path of the mapping where to prepend.\n     * @param string $prependedPath The path of the mapping to prepend.\n     */\n    private function prependOrderEntry($path, $prependedPath)\n    {\n        $lastEntry = reset($this->json['_order'][$path]);\n\n        if ($prependedPath === $lastEntry['path']) {\n            // If the first entry matches the new one, add the reference\n            // of the current resource to the limit\n            ++$lastEntry['references'];\n        } else {\n            array_unshift($this->json['_order'][$path], array(\n                'path' => $prependedPath,\n                'references' => 1,\n            ));\n        }\n    }\n\n    /**\n     * Initializes a path with the order of the closest parent path.\n     *\n     * @param string $path             The path to initialize.\n     * @param array  $parentReferences The defined references for parent paths,\n     *                                 with long paths /a/b sorted before short\n     *                                 paths /a.\n     */\n    private function initWithParentOrder($path, array $parentReferences)\n    {\n        foreach ($parentReferences as $parentPath => $_) {\n            // Look for the first parent entry for which an order is defined\n            if (isset($this->json['_order'][$parentPath])) {\n                // Inherit that order\n                $this->json['_order'][$path] = $this->json['_order'][$parentPath];\n\n                return;\n            }\n        }\n    }\n\n    /**\n     * Initializes the order of a path with the default order.\n     *\n     * This is necessary if we want to insert a non-default order entry for\n     * the first time.\n     *\n     * @param string $path         The path to initialize.\n     * @param string $insertedPath The path that is being inserted.\n     * @param array  $references   The references for each defined path mapping\n     *                             in the path chain.\n     */\n    private function initWithDefaultOrder($path, $insertedPath, $references)\n    {\n        $this->json['_order'][$path] = array();\n\n        // Insert the default order, if none exists\n        // i.e. long paths /a/b/c before short paths /a/b\n        $parentPath = $path;\n\n        while (true) {\n            if (isset($references[$parentPath])) {\n                $parentEntry = array(\n                    'path' => $parentPath,\n                    'references' => count($references[$parentPath]),\n                );\n\n                // Edge case: $parentPath equals $insertedPath. In this case we have\n                // to subtract the entry that we're adding\n                if ($parentPath === $insertedPath) {\n                    --$parentEntry['references'];\n                }\n\n                if (0 !== $parentEntry['references']) {\n                    $this->json['_order'][$path][] = $parentEntry;\n                }\n            }\n\n            if ('/' === $parentPath) {\n                break;\n            }\n\n            $parentPath = Path::getDirectory($parentPath);\n        }\n    }\n}\n"
  },
  {
    "path": "src/NullRepository.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository;\n\nuse Puli\\Repository\\Api\\EditableRepository;\nuse Puli\\Repository\\Api\\NoVersionFoundException;\nuse Puli\\Repository\\Api\\ResourceNotFoundException;\nuse Puli\\Repository\\Resource\\Collection\\ArrayResourceCollection;\n\n/**\n * A repository that does nothing.\n *\n * This repository can be used if you need to inject a repository instance in\n * some code, but you don't want that repository to do anything (for example\n * in tests).\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass NullRepository implements EditableRepository\n{\n    /**\n     * {@inheritdoc}\n     */\n    public function add($path, $resource)\n    {\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function remove($query, $language = 'glob')\n    {\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function clear()\n    {\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function get($path)\n    {\n        throw ResourceNotFoundException::forPath($path);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getVersions($path)\n    {\n        throw NoVersionFoundException::forPath($path);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function find($query, $language = 'glob')\n    {\n        return new ArrayResourceCollection();\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function contains($query, $language = 'glob')\n    {\n        return false;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function hasChildren($path)\n    {\n        return false;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function listChildren($path)\n    {\n        return new ArrayResourceCollection();\n    }\n}\n"
  },
  {
    "path": "src/OptimizedJsonRepository.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository;\n\nuse Puli\\Repository\\Api\\EditableRepository;\nuse Puli\\Repository\\Api\\Resource\\FilesystemResource;\nuse Webmozart\\Glob\\Glob;\nuse Webmozart\\PathUtil\\Path;\n\n/**\n * A repository backed by a JSON file optimized for reading.\n *\n * The generated JSON file is described by res/schema/repository-schema-1.0.json.\n *\n * Resources can be added with the method {@link add()}:\n *\n * ```php\n * use Puli\\Repository\\OptimizedJsonRepository;\n *\n * $repo = new OptimizedJsonRepository('/path/to/repository.json', '/path/to/project');\n * $repo->add('/css', new DirectoryResource('/path/to/project/res/css'));\n * ```\n *\n * When adding a resource, the added filesystem path is stored in the JSON file\n * under the key of the Puli path. The path is stored relatively to the base\n * directory passed to the constructor. Directories will be expanded and all\n * nested files will be added to the mapping file as well:\n *\n * ```json\n * {\n *     \"/css\": \"res/css\",\n *     \"/css/style.css\": \"res/css/style.css\"\n * }\n * ```\n *\n * Mapped resources can be read with the method {@link get()}:\n *\n * ```php\n * $cssPath = $repo->get('/css')->getFilesystemPath();\n * ```\n *\n * You can also access nested files:\n *\n * ```php\n * echo $repo->get('/css/style.css')->getBody();\n * ```\n *\n * Since nested files are searched during {@link add()} and added to the JSON\n * file, this repository does not detect any files that you add to a directory\n * after adding that directory to the repository. This means that accessing\n * files is very fast, but also that the usage of this repository implementation\n * can be cumbersome in development environments. There you are recommended to\n * use {@link JsonRepository} instead.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n * @author Titouan Galopin <galopintitouan@gmail.com>\n */\nclass OptimizedJsonRepository extends AbstractJsonRepository implements EditableRepository\n{\n    /**\n     * {@inheritdoc}\n     */\n    public function clear()\n    {\n        if (null === $this->json) {\n            $this->load();\n        }\n\n        // Subtract root which is not deleted\n        $removed = count($this->json) - 1;\n\n        $this->json = array();\n\n        $this->flush();\n\n        $this->clearVersions();\n        $this->storeVersion($this->get('/'));\n\n        return $removed;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    protected function insertReference($path, $reference)\n    {\n        $this->json[$path] = $reference;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    protected function removeReferences($glob)\n    {\n        $removed = 0;\n\n        foreach ($this->getReferencesForGlob($glob.'{,/**/*}') as $path => $reference) {\n            ++$removed;\n\n            $this->removeVersions($path);\n\n            unset($this->json[$path]);\n        }\n\n        return $removed;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    protected function getReferencesForPath($path)\n    {\n        if (!array_key_exists($path, $this->json)) {\n            return array();\n        }\n\n        $reference = $this->json[$path];\n\n        // We're only interested in the first entry of eventual arrays\n        if (is_array($reference)) {\n            $reference = reset($reference);\n        }\n\n        if ($this->isFilesystemReference($reference)) {\n            $absoluteReference = Path::makeAbsolute($reference, $this->baseDirectory);\n\n            if (!file_exists($absoluteReference)) {\n                $this->logReferenceNotFound($path, $reference, $absoluteReference);\n\n                return array();\n            }\n\n            $reference = $absoluteReference;\n        }\n\n        return array($path => $reference);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    protected function getReferencesForGlob($glob, $flags = 0)\n    {\n        if (!Glob::isDynamic($glob)) {\n            return $this->getReferencesForPath($glob);\n        }\n\n        return $this->getReferencesForRegex(\n            Glob::getStaticPrefix($glob),\n            Glob::toRegEx($glob),\n            $flags\n        );\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    protected function getReferencesForRegex($staticPrefix, $regex, $flags = 0)\n    {\n        $result = array();\n        $foundMappingsWithPrefix = false;\n\n        foreach ($this->json as $path => $reference) {\n            if (0 === strpos($path, $staticPrefix)) {\n                $foundMappingsWithPrefix = true;\n\n                if (!preg_match($regex, $path)) {\n                    continue;\n                }\n\n                // We're only interested in the first entry of eventual arrays\n                if (is_array($reference)) {\n                    $reference = reset($reference);\n                }\n\n                if ($this->isFilesystemReference($reference)) {\n                    $absoluteReference = Path::makeAbsolute($reference, $this->baseDirectory);\n\n                    if (!file_exists($absoluteReference)) {\n                        $this->logReferenceNotFound($path, $reference, $absoluteReference);\n\n                        continue;\n                    }\n\n                    $reference = $absoluteReference;\n                }\n\n                $result[$path] = $reference;\n\n                if ($flags & self::STOP_ON_FIRST) {\n                    return $result;\n                }\n\n                continue;\n            }\n\n            // We did not find anything but previously found mappings with the\n            // static prefix\n            // The mappings are sorted alphabetically, so we can safely abort\n            if ($foundMappingsWithPrefix) {\n                break;\n            }\n        }\n\n        return $result;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    protected function getReferencesInDirectory($path, $flags = 0)\n    {\n        $basePath = rtrim($path, '/');\n\n        return $this->getReferencesForRegex(\n            $basePath.'/',\n            '~^'.preg_quote($basePath, '~').'/[^/]+$~',\n            $flags\n        );\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    protected function addFilesystemResource($path, FilesystemResource $resource)\n    {\n        // Read children before attaching the resource to this repository\n        $children = $resource->listChildren();\n\n        parent::addFilesystemResource($path, $resource);\n\n        // Recursively add all child resources\n        $basePath = '/' === $path ? $path : $path.'/';\n\n        foreach ($children as $name => $child) {\n            $this->addFilesystemResource($basePath.$name, $child);\n        }\n    }\n}\n"
  },
  {
    "path": "src/RepositoryFactoryException.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository;\n\nuse RuntimeException;\n\n/**\n * Thrown if a factory closure did not create valid repository.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass RepositoryFactoryException extends RuntimeException\n{\n}\n"
  },
  {
    "path": "src/Resource/AbstractFilesystemResource.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Resource;\n\nuse Puli\\Repository\\Api\\Resource\\FilesystemResource;\nuse Puli\\Repository\\Resource\\Metadata\\FilesystemMetadata;\n\n/**\n * Base class for filesystem resources.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nabstract class AbstractFilesystemResource extends GenericResource implements FilesystemResource\n{\n    /**\n     * @var string\n     */\n    private $filesystemPath;\n\n    /**\n     * Creates a new filesystem resource.\n     *\n     * @param string      $filesystemPath The path on the file system.\n     * @param string|null $path           The repository path of the resource.\n     */\n    public function __construct($filesystemPath, $path = null)\n    {\n        parent::__construct($path);\n\n        $this->filesystemPath = str_replace(DIRECTORY_SEPARATOR, '/', $filesystemPath);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getFilesystemPath()\n    {\n        return $this->filesystemPath;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getMetadata()\n    {\n        return new FilesystemMetadata($this->filesystemPath);\n    }\n\n    protected function preSerialize(array &$data)\n    {\n        parent::preSerialize($data);\n\n        $data[] = $this->filesystemPath;\n    }\n\n    protected function postUnserialize(array $data)\n    {\n        $this->filesystemPath = array_pop($data);\n\n        parent::postUnserialize($data);\n    }\n}\n"
  },
  {
    "path": "src/Resource/Collection/ArrayResourceCollection.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Resource\\Collection;\n\nuse InvalidArgumentException;\nuse IteratorAggregate;\nuse OutOfBoundsException;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\nuse Puli\\Repository\\Api\\ResourceCollection;\nuse Puli\\Repository\\Api\\UnsupportedResourceException;\nuse Puli\\Repository\\Resource\\Iterator\\ResourceCollectionIterator;\nuse Traversable;\nuse Webmozart\\Assert\\Assert;\n\n/**\n * A collection of {@link PuliResource} instances backed by an array.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass ArrayResourceCollection implements IteratorAggregate, ResourceCollection\n{\n    /**\n     * @var PuliResource[]|Traversable\n     */\n    private $resources;\n\n    /**\n     * Creates a new collection.\n     *\n     * You can pass the resources that you want to initially store in the\n     * collection as argument.\n     *\n     * @param PuliResource[]|Traversable $resources The resources to store in the collection.\n     *\n     * @throws InvalidArgumentException     If the resources are not an array\n     *                                      and not a traversable object.\n     * @throws UnsupportedResourceException If a resource does not implement\n     *                                      {@link PuliResource}.\n     */\n    public function __construct($resources = array())\n    {\n        $this->replace($resources);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function add(PuliResource $resource)\n    {\n        $this->resources[] = $resource;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function set($key, PuliResource $resource)\n    {\n        $this->resources[$key] = $resource;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function get($key)\n    {\n        if (!isset($this->resources[$key])) {\n            throw new OutOfBoundsException(sprintf(\n                'The offset \"%s\" does not exist.',\n                $key\n            ));\n        }\n\n        return $this->resources[$key];\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function remove($key)\n    {\n        unset($this->resources[$key]);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function has($key)\n    {\n        return isset($this->resources[$key]);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function clear()\n    {\n        $this->resources = array();\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function keys()\n    {\n        return array_keys($this->resources);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function replace($resources)\n    {\n        Assert::allIsInstanceOf($resources, 'Puli\\Repository\\Api\\Resource\\PuliResource');\n\n        $this->resources = is_array($resources) ? $resources : iterator_to_array($resources);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function merge($resources)\n    {\n        Assert::allIsInstanceOf($resources, 'Puli\\Repository\\Api\\Resource\\PuliResource');\n\n        // only start merging after validating all resources\n        foreach ($resources as $resource) {\n            $this->resources[] = $resource;\n        }\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function isEmpty()\n    {\n        return 0 === count($this->resources);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function offsetExists($key)\n    {\n        return $this->has($key);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function offsetGet($key)\n    {\n        return $this->get($key);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function offsetSet($key, $value)\n    {\n        if (null !== $key) {\n            $this->set($key, $value);\n        } else {\n            $this->add($value);\n        }\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function offsetUnset($key)\n    {\n        $this->remove($key);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getPaths()\n    {\n        return array_map(\n            function (PuliResource $resource) {\n                return $resource->getPath();\n            },\n            $this->resources\n        );\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getNames()\n    {\n        return array_map(\n            function (PuliResource $resource) {\n                return $resource->getName();\n            },\n            $this->resources\n        );\n    }\n\n    public function count()\n    {\n        return count($this->resources);\n    }\n\n    public function getIterator($mode = ResourceCollectionIterator::KEY_AS_CURSOR)\n    {\n        return new ResourceCollectionIterator($this, $mode);\n    }\n\n    public function toArray()\n    {\n        return $this->resources;\n    }\n}\n"
  },
  {
    "path": "src/Resource/Collection/FilesystemResourceCollection.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Resource\\Collection;\n\nuse Puli\\Repository\\Api\\Resource\\FilesystemResource;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\n\n/**\n * A collection of resources on the filesystem.\n *\n * The resource collection contains the additional method\n * {@link getFilesystemPaths()} for batch collecting the filesystem paths of all\n * contained resources.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass FilesystemResourceCollection extends ArrayResourceCollection\n{\n    /**\n     * Returns the filesystem paths of all contained resources.\n     *\n     * The paths are returned in order of the resources. Resources that are not\n     * on the filesystem are ignored and not contained in the output.\n     *\n     * @return string[] The filesystem paths.\n     */\n    public function getFilesystemPaths()\n    {\n        return array_map(\n            function (FilesystemResource $resource) {\n                return $resource->getFilesystemPath();\n            },\n            array_filter(\n                $this->toArray(),\n                function (PuliResource $r) {\n                    return $r instanceof FilesystemResource && null !== $r->getFilesystemPath();\n                }\n            )\n        );\n    }\n}\n"
  },
  {
    "path": "src/Resource/Collection/LazyResourceCollection.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Resource\\Collection;\n\nuse BadMethodCallException;\nuse IteratorAggregate;\nuse OutOfBoundsException;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\nuse Puli\\Repository\\Api\\ResourceCollection;\nuse Puli\\Repository\\Api\\ResourceRepository;\nuse Puli\\Repository\\Resource\\Iterator\\ResourceCollectionIterator;\nuse Traversable;\n\n/**\n * A resource collection which loads its resources on demand.\n *\n * This collection is read-only. Each resource is loaded when it is accessed\n * for the first time.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass LazyResourceCollection implements IteratorAggregate, ResourceCollection\n{\n    /**\n     * @var string[]|PuliResource[]\n     */\n    private $resources;\n\n    /**\n     * @var ResourceRepository\n     */\n    private $repo;\n\n    /**\n     * @var bool\n     */\n    private $loaded = false;\n\n    /**\n     * Creates a new collection.\n     *\n     * @param ResourceRepository $repo  The repository that will be used to load\n     *                                  the resources.\n     * @param array              $paths The paths of the resources which will be\n     *                                  loaded into the collection.\n     */\n    public function __construct(ResourceRepository $repo, array $paths)\n    {\n        $this->resources = $paths;\n        $this->repo = $repo;\n    }\n\n    /**\n     * Not supported.\n     *\n     * @param PuliResource $resource The resource to add.\n     *\n     * @throws BadMethodCallException The collection is read-only.\n     */\n    public function add(PuliResource $resource)\n    {\n        throw new BadMethodCallException(\n            'Lazy resource collections cannot be modified.'\n        );\n    }\n\n    /**\n     * Not supported.\n     *\n     * @param int          $key      The collection key.\n     * @param PuliResource $resource The resource to add.\n     *\n     * @throws BadMethodCallException The collection is read-only.\n     */\n    public function set($key, PuliResource $resource)\n    {\n        throw new BadMethodCallException(\n            'Lazy resource collections cannot be modified.'\n        );\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function get($key)\n    {\n        if (!isset($this->resources[$key])) {\n            throw new OutOfBoundsException(sprintf(\n                'The offset \"%s\" does not exist.',\n                $key\n            ));\n        }\n\n        if (!$this->resources[$key] instanceof PuliResource) {\n            $this->resources[$key] = $this->repo->get($this->resources[$key]);\n        }\n\n        return $this->resources[$key];\n    }\n\n    /**\n     * Not supported.\n     *\n     * @param string $key The collection key to remove.\n     *\n     * @throws BadMethodCallException The collection is read-only.\n     */\n    public function remove($key)\n    {\n        throw new BadMethodCallException(\n            'Lazy resource collections cannot be modified.'\n        );\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function has($key)\n    {\n        return isset($this->resources[$key]);\n    }\n\n    /**\n     * Not supported.\n     *\n     * @throws BadMethodCallException The collection is read-only.\n     */\n    public function clear()\n    {\n        throw new BadMethodCallException(\n            'Lazy resource collections cannot be modified.'\n        );\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function keys()\n    {\n        if (!$this->loaded) {\n            $this->load();\n        }\n\n        return array_keys($this->resources);\n    }\n\n    /**\n     * Not supported.\n     *\n     * @param PuliResource[]|Traversable $resources The resources to replace the\n     *                                              collection contents with.\n     *\n     * @throws BadMethodCallException The collection is read-only.\n     */\n    public function replace($resources)\n    {\n        throw new BadMethodCallException(\n            'Lazy resource collections cannot be modified.'\n        );\n    }\n\n    /**\n     * Not supported.\n     *\n     * @param PuliResource[]|Traversable $resources The resources to merge into\n     *                                              the collection.\n     *\n     * @throws BadMethodCallException The collection is read-only.\n     */\n    public function merge($resources)\n    {\n        throw new BadMethodCallException(\n            'Lazy resource collections cannot be modified.'\n        );\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function isEmpty()\n    {\n        return 0 === count($this->resources);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function offsetExists($key)\n    {\n        return $this->has($key);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function offsetGet($key)\n    {\n        return $this->get($key);\n    }\n\n    /**\n     * Not supported.\n     *\n     * @param string       $key      The collection key to set.\n     * @param PuliResource $resource The resource to set.\n     *\n     * @throws BadMethodCallException The collection is read-only.\n     */\n    public function offsetSet($key, $resource)\n    {\n        throw new BadMethodCallException(\n            'Lazy resource collections cannot be modified.'\n        );\n    }\n\n    /**\n     * Not supported.\n     *\n     * @param string $key The collection key to remove.\n     *\n     * @throws BadMethodCallException The collection is read-only.\n     */\n    public function offsetUnset($key)\n    {\n        throw new BadMethodCallException(\n            'Lazy resource collections cannot be modified.'\n        );\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getPaths()\n    {\n        if (!$this->loaded) {\n            $this->load();\n        }\n\n        return array_map(\n            function (PuliResource $resource) {\n                return $resource->getPath();\n            },\n            $this->resources\n        );\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getNames()\n    {\n        if (!$this->loaded) {\n            $this->load();\n        }\n\n        return array_map(\n            function (PuliResource $resource) {\n                return $resource->getName();\n            },\n            $this->resources\n        );\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getIterator($mode = ResourceCollectionIterator::KEY_AS_CURSOR)\n    {\n        if (!$this->loaded) {\n            $this->load();\n        }\n\n        return new ResourceCollectionIterator($this, $mode);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function toArray()\n    {\n        if (!$this->loaded) {\n            $this->load();\n        }\n\n        return $this->resources;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function count()\n    {\n        return count($this->resources);\n    }\n\n    /**\n     * Loads the complete collection.\n     */\n    private function load()\n    {\n        foreach ($this->resources as $key => $resource) {\n            if (!$resource instanceof PuliResource) {\n                $this->resources[$key] = $this->repo->get($resource);\n            }\n        }\n\n        $this->loaded = true;\n    }\n}\n"
  },
  {
    "path": "src/Resource/DirectoryResource.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Resource;\n\nuse Puli\\Repository\\Api\\ResourceNotFoundException;\nuse Puli\\Repository\\Resource\\Collection\\FilesystemResourceCollection;\nuse Webmozart\\Assert\\Assert;\nuse Webmozart\\Glob\\Iterator\\RecursiveDirectoryIterator;\n\n/**\n * Represents a directory on the file system.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass DirectoryResource extends AbstractFilesystemResource\n{\n    /**\n     * {@inheritdoc}\n     */\n    public function __construct($filesystemPath, $path = null)\n    {\n        Assert::directory($filesystemPath);\n\n        parent::__construct($filesystemPath, $path);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getChild($relPath)\n    {\n        // Use attached repository if possible\n        if ($this->getRepository()) {\n            return $this->getRepository()->get($this->getRepositoryPath().'/'.$relPath);\n        }\n\n        $filesystemPath = $this->getFilesystemPath().'/'.$relPath;\n\n        if (!file_exists($filesystemPath)) {\n            throw ResourceNotFoundException::forPath($this->getPath().'/'.$relPath);\n        }\n\n        $childPath = null === $this->getPath() ? null : $this->getPath().'/'.$relPath;\n\n        return is_dir($filesystemPath)\n            ? new self($filesystemPath, $childPath)\n            : new FileResource($filesystemPath, $childPath);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function hasChild($relPath)\n    {\n        // Use attached repository if possible\n        if ($this->getRepository()) {\n            return $this->getRepository()->contains($this->getRepositoryPath().'/'.$relPath);\n        }\n\n        return file_exists($this->getFilesystemPath().'/'.$relPath);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function hasChildren()\n    {\n        // Use attached repository if possible\n        if ($this->getRepository()) {\n            return $this->getRepository()->hasChildren($this->getRepositoryPath());\n        }\n\n        $iterator = new RecursiveDirectoryIterator(\n            $this->getFilesystemPath(),\n            RecursiveDirectoryIterator::SKIP_DOTS\n        );\n        $iterator->rewind();\n\n        return $iterator->valid();\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function listChildren()\n    {\n        $children = new FilesystemResourceCollection();\n\n        // Use attached repository if possible\n        if ($this->getRepository()) {\n            foreach ($this->getRepository()->listChildren($this->getRepositoryPath()) as $child) {\n                $children[$child->getName()] = $child;\n            }\n\n            return $children;\n        }\n\n        $iterator = new RecursiveDirectoryIterator(\n            $this->getFilesystemPath(),\n            RecursiveDirectoryIterator::CURRENT_AS_PATHNAME | RecursiveDirectoryIterator::SKIP_DOTS\n        );\n\n        // We can't use glob() here, because glob() doesn't list files starting\n        // with \".\" by default\n        foreach ($iterator as $path) {\n            $children[basename($path)] = is_dir($path)\n                ? new self($path)\n                : new FileResource($path);\n        }\n\n        return $children;\n    }\n}\n"
  },
  {
    "path": "src/Resource/FileResource.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Resource;\n\nuse Puli\\Repository\\Api\\Resource\\BodyResource;\nuse Puli\\Repository\\Api\\ResourceNotFoundException;\nuse Puli\\Repository\\Resource\\Collection\\ArrayResourceCollection;\nuse Webmozart\\Assert\\Assert;\n\n/**\n * Represents a file on the file system.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass FileResource extends AbstractFilesystemResource implements BodyResource\n{\n    /**\n     * {@inheritdoc}\n     */\n    public function __construct($filesystemPath, $path = null)\n    {\n        Assert::file($filesystemPath);\n\n        parent::__construct($filesystemPath, $path);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getBody()\n    {\n        return file_get_contents($this->getFilesystemPath());\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getChild($relPath)\n    {\n        throw ResourceNotFoundException::forPath($this->getPath().'/'.$relPath);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function hasChild($relPath)\n    {\n        return false;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function hasChildren()\n    {\n        return false;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function listChildren()\n    {\n        return new ArrayResourceCollection();\n    }\n}\n"
  },
  {
    "path": "src/Resource/GenericResource.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Resource;\n\nuse Puli\\Repository\\Api\\ChangeStream\\VersionList;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\nuse Puli\\Repository\\Api\\Resource\\ResourceMetadata;\nuse Puli\\Repository\\Api\\ResourceNotFoundException;\nuse Puli\\Repository\\Api\\ResourceRepository;\nuse Puli\\Repository\\Resource\\Collection\\ArrayResourceCollection;\n\n/**\n * A generic resource.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass GenericResource implements PuliResource\n{\n    /**\n     * @var ResourceRepository\n     */\n    private $repo;\n\n    /**\n     * @var string\n     */\n    private $path;\n\n    /**\n     * @var string\n     */\n    private $repoPath;\n\n    /**\n     * Creates a new resource.\n     *\n     * @param string|null $path The path of the resource.\n     */\n    public function __construct($path = null)\n    {\n        $this->path = $path;\n        $this->repoPath = $path;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getPath()\n    {\n        return $this->path;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getName()\n    {\n        return $this->path ? basename($this->path) : null;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getChild($relPath)\n    {\n        if (!$this->getRepository()) {\n            throw ResourceNotFoundException::forPath($this->getRepositoryPath().'/'.$relPath);\n        }\n\n        return $this->getRepository()->get($this->getRepositoryPath().'/'.$relPath);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function hasChild($relPath)\n    {\n        if (!$this->getRepository()) {\n            return false;\n        }\n\n        return $this->getRepository()->contains($this->getRepositoryPath().'/'.$relPath);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function hasChildren()\n    {\n        if (!$this->getRepository()) {\n            return false;\n        }\n\n        return $this->getRepository()->hasChildren($this->getRepositoryPath());\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function listChildren()\n    {\n        $children = new ArrayResourceCollection();\n\n        if (!$this->getRepository()) {\n            return $children;\n        }\n\n        foreach ($this->getRepository()->listChildren($this->getRepositoryPath()) as $child) {\n            $children[$child->getName()] = $child;\n        }\n\n        return $children;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getVersions()\n    {\n        if (!$this->getRepository()) {\n            return new VersionList($this->getRepositoryPath(), array($this));\n        }\n\n        return $this->getRepository()->getVersions($this->getRepositoryPath());\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getMetadata()\n    {\n        return new ResourceMetadata();\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function attachTo(ResourceRepository $repo, $path = null)\n    {\n        $this->repo = $repo;\n\n        if (null !== $path) {\n            $this->path = $path;\n            $this->repoPath = $path;\n        }\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function detach()\n    {\n        $this->repo = null;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getRepository()\n    {\n        return $this->repo;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getRepositoryPath()\n    {\n        return $this->repoPath;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function isAttached()\n    {\n        return null !== $this->repo;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function createReference($path)\n    {\n        $ref = clone $this;\n        $ref->path = $path;\n\n        return $ref;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function isReference()\n    {\n        return $this->path !== $this->repoPath;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function serialize()\n    {\n        $data = array();\n\n        $this->preSerialize($data);\n\n        return serialize($data);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function unserialize($string)\n    {\n        $data = unserialize($string);\n\n        $this->postUnserialize($data);\n    }\n\n    /**\n     * Invoked before serializing a resource.\n     *\n     * Override this method if you want to serialize custom data in subclasses.\n     *\n     * @param array $data The data to serialize. Add custom data at the end of\n     *                    the array.\n     */\n    protected function preSerialize(array &$data)\n    {\n        $data[] = $this->path;\n        $data[] = $this->repoPath;\n    }\n\n    /**\n     * Invoked after unserializing a resource.\n     *\n     * Override this method if you want to unserialize custom data in\n     * subclasses.\n     *\n     * @param array $data The unserialized data. Pop your custom data from the\n     *                    end of the array before calling the parent method.\n     */\n    protected function postUnserialize(array $data)\n    {\n        $this->repoPath = array_pop($data);\n        $this->path = array_pop($data);\n    }\n}\n"
  },
  {
    "path": "src/Resource/Iterator/RecursiveResourceIterator.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Resource\\Iterator;\n\nuse Puli\\Repository\\Api\\ResourceIterator;\nuse RecursiveIterator;\n\n/**\n * A resource iterator that can be iterated recursively.\n *\n * Use {@link RecursiveResourceIteratorIterator} to iterate over the iterator.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\ninterface RecursiveResourceIterator extends ResourceIterator, RecursiveIterator\n{\n}\n"
  },
  {
    "path": "src/Resource/Iterator/RecursiveResourceIteratorIterator.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Resource\\Iterator;\n\nuse Puli\\Repository\\Api\\ResourceIterator;\nuse RecursiveIteratorIterator;\n\n/**\n * Iterates recursively over {@link RecursiveResourceIterator} instances.\n *\n * Use this iterator to iterate recursively over a recursive resource iterator:\n *\n * ```php\n * $iterator = new RecursiveResourceIteratorIterator(\n *     new ResourceCollectionIterator(\n *         $collection,\n *         ResourceCollectionIterator::KEY_AS_PATH | ResourceCollectionIterator::CURRENT_AS_RESOURCE\n *     ),\n *     RecursiveResourceIteratorIterator::SELF_FIRST\n * );\n *\n * foreach ($iterator as $path => $resource) {\n *     // ...\n * }\n * ```\n *\n * The configuration of this iterator works identically to its parent class\n * {@link RecursiveIteratorIterator}.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass RecursiveResourceIteratorIterator extends RecursiveIteratorIterator implements ResourceIterator\n{\n    /**\n     * Creates a new iterator.\n     *\n     * @param RecursiveResourceIterator $iterator The inner iterator.\n     * @param int                       $mode     The iteration mode.\n     * @param int                       $flags    The iteration flags.\n     *\n     * @see RecursiveIteratorIterator::__construct\n     */\n    public function __construct(RecursiveResourceIterator $iterator, $mode = self::LEAVES_ONLY, $flags = 0)\n    {\n        parent::__construct($iterator, $mode, $flags);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getCurrentResource()\n    {\n        return $this->getInnerIterator()->getCurrentResource();\n    }\n}\n"
  },
  {
    "path": "src/Resource/Iterator/ResourceCollectionIterator.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Resource\\Iterator;\n\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\nuse Puli\\Repository\\Api\\ResourceCollection;\n\n/**\n * A recursive iterator for resource collections.\n *\n * Use the iterator if you want to iterate a resource collection. You can\n * configure what the iterator should return as keys and values:\n *\n * ```php\n * $iterator = new ResourceCollectionIterator(\n *     $collection,\n *     ResourceCollectionIterator::KEY_AS_PATH | ResourceCollectionIterator::CURRENT_AS_RESOURCE\n * );\n *\n * foreach ($iterator as $path => $resource) {\n *     // ...\n * }\n * ```\n *\n * If you want to iterate the collection recursively, wrap it in a\n * {@link RecursiveResourceIteratorIterator}:\n *\n * ```php\n * $iterator = new RecursiveResourceIteratorIterator(\n *     new ResourceCollectionIterator(\n *         $collection,\n *         ResourceCollectionIterator::KEY_AS_PATH | ResourceCollectionIterator::CURRENT_AS_RESOURCE\n *     ),\n *     RecursiveResourceIteratorIterator::SELF_FIRST\n * );\n *\n * foreach ($iterator as $path => $resource) {\n *     // ...\n * }\n * ```\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass ResourceCollectionIterator implements RecursiveResourceIterator\n{\n    /**\n     * Return {@link PuliResource} instances as values.\n     */\n    const CURRENT_AS_RESOURCE = 1;\n\n    /**\n     * Return the paths of the resources as values.\n     */\n    const CURRENT_AS_PATH = 2;\n\n    /**\n     * Return the names of the resources as values.\n     */\n    const CURRENT_AS_NAME = 4;\n\n    /**\n     * Return the paths of the resources as keys.\n     */\n    const KEY_AS_PATH = 64;\n\n    /**\n     * Return the collection keys as keys.\n     *\n     * Attention: Don't use this mode when iterating recursively, as PHP's\n     * {@link RecursiveIteratorIterator} skips inner nodes then.\n     */\n    const KEY_AS_CURSOR = 128;\n\n    /**\n     * @var PuliResource[]\n     */\n    protected $resources;\n\n    /**\n     * @var int\n     */\n    protected $mode;\n\n    /**\n     * Creates a new iterator.\n     *\n     * The following constants can be used to configure the values returned by\n     * the iterator:\n     *\n     *  * {@link CURRENT_AS_RESOURCE}: The {@link PuliResource} objects are\n     *                                 returned as values;\n     *  * {@link CURRENT_AS_PATH}: The resource paths are returned as values;\n     *  * {@link CURRENT_AS_NAME}: The resource names are returned as values.\n     *\n     * The following constants can be used to configure the keys returned by\n     * the iterator:\n     *\n     *  * {@link KEY_AS_CURSOR}: The collection keys are returned as keys;\n     *  * {@link KEY_AS_PATH}: The resource paths are returned as keys.\n     *\n     * By default, the mode `KEY_AS_PATH | CURRENT_AS_RESOURCE` is used.\n     *\n     * @param ResourceCollection $resources The resources to iterate.\n     * @param int|null           $mode      A bitwise combination of the mode\n     *                                      constants.\n     */\n    public function __construct(ResourceCollection $resources, $mode = null)\n    {\n        if (!($mode & (self::CURRENT_AS_PATH | self::CURRENT_AS_RESOURCE | self::CURRENT_AS_NAME))) {\n            $mode |= self::CURRENT_AS_RESOURCE;\n        }\n\n        if (!($mode & (self::KEY_AS_PATH | self::KEY_AS_CURSOR))) {\n            $mode |= self::KEY_AS_PATH;\n        }\n\n        $this->resources = $resources->toArray();\n        $this->mode = $mode;\n    }\n\n    /**\n     * Returns the current value of the iterator.\n     *\n     * @return PuliResource|string The current value as configured in\n     *                             {@link __construct}.\n     */\n    public function current()\n    {\n        if ($this->mode & self::CURRENT_AS_RESOURCE) {\n            return current($this->resources);\n        }\n\n        if ($this->mode & self::CURRENT_AS_PATH) {\n            return current($this->resources)->getPath();\n        }\n\n        return current($this->resources)->getName();\n    }\n\n    /**\n     * Advances the iterator to the next position.\n     */\n    public function next()\n    {\n        next($this->resources);\n    }\n\n    /**\n     * Returns the current key of the iterator.\n     *\n     * @return int|string|null The current key as configured in\n     *                         {@link __construct} or `null` if the cursor\n     *                         is behind the last element.\n     */\n    public function key()\n    {\n        if (null === ($key = key($this->resources))) {\n            return null;\n        }\n\n        if ($this->mode & self::KEY_AS_PATH) {\n            return $this->resources[$key]->getPath();\n        }\n\n        return $key;\n    }\n\n    /**\n     * Returns whether the iterator points to a valid key.\n     *\n     * @return bool Whether the iterator position is valid.\n     */\n    public function valid()\n    {\n        return null !== key($this->resources);\n    }\n\n    /**\n     * Rewinds the iterator to the first entry.\n     */\n    public function rewind()\n    {\n        reset($this->resources);\n    }\n\n    /**\n     * Returns whether the iterator can be applied recursively over the\n     * current element.\n     *\n     * @return bool Whether the current element can be iterated recursively.\n     */\n    public function hasChildren()\n    {\n        return current($this->resources)->hasChildren();\n    }\n\n    /**\n     * Returns the iterator for the children of the current element.\n     *\n     * @return static Returns an instance of this class for the children of\n     *                the current element.\n     */\n    public function getChildren()\n    {\n        return new static(current($this->resources)->listChildren(), $this->mode);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getCurrentResource()\n    {\n        return current($this->resources);\n    }\n}\n"
  },
  {
    "path": "src/Resource/Iterator/ResourceFilterIterator.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Resource\\Iterator;\n\nuse FilterIterator;\nuse Puli\\Repository\\Api\\ResourceIterator;\nuse Webmozart\\Assert\\Assert;\n\n/**\n * Iterates over a {@link ResourceIterator} and filters out individual entries.\n *\n * You can use the iterator to filter files with a specific extension:\n *\n * ```php\n * $iterator = new ResourceFilterIterator(\n *     new RecursiveResourceIteratorIterator(\n *         new ResourceCollectionIterator($collection),\n *     ),\n *     '.css',\n *     ResourceFilterIterator::MATCH_SUFFIX\n * );\n *\n * foreach ($iterator as $path => $resource) {\n *     // ...\n * }\n * ```\n *\n * See {@link __construct} for more information on the filter options.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass ResourceFilterIterator extends FilterIterator implements ResourceIterator\n{\n    /**\n     * Matches the pattern against the resource path.\n     */\n    const FILTER_BY_PATH = 1;\n\n    /**\n     * Matches the pattern against the resource name.\n     */\n    const FILTER_BY_NAME = 2;\n\n    /**\n     * Includes resources if the pattern is a prefix of the matched text.\n     */\n    const MATCH_PREFIX = 32;\n\n    /**\n     * Includes resources if the pattern is a suffix of the matched text.\n     */\n    const MATCH_SUFFIX = 64;\n\n    /**\n     * Includes resources if the matched text satisfies the pattern as regular\n     * expression.\n     */\n    const MATCH_REGEX = 128;\n\n    /**\n     * @var string\n     */\n    private $pattern;\n\n    /**\n     * @var int\n     */\n    private $patternLength;\n\n    /**\n     * @var int\n     */\n    private $mode;\n\n    /**\n     * Creates a new iterator.\n     *\n     * The following constants can be used to configure what to filter by:\n     *\n     *  * {@link FILTER_BY_PATH}: The pattern is matched against the paths;\n     *  * {@link FILTER_BY_NAME}: The pattern is matched against the names.\n     *\n     * The following constants can be used to configure how to match the\n     * selected text:\n     *\n     *  * {@link MATCH_PREFIX}: Tests whether the pattern is a prefix of the\n     *                          matched text;\n     *  * {@link MATCH_SUFFIX}: Tests whether the pattern is a suffix of the\n     *                          matched text;\n     *  * {@link MATCH_REGEX}: Treats the pattern as regular expression.\n     *\n     * By default, the mode `FILTER_BY_PATH | MATCH_REGEX` is used.\n     *\n     * @param ResourceIterator $iterator The filtered iterator.\n     * @param string           $pattern  The pattern to match.\n     * @param int|null         $mode     A bitwise combination of the mode\n     *                                   constants.\n     */\n    public function __construct(ResourceIterator $iterator, $pattern, $mode = null)\n    {\n        Assert::stringNotEmpty($pattern, 'The pattern must be a non-empty string. Got: %s');\n\n        parent::__construct($iterator);\n\n        if (!($mode & (self::FILTER_BY_PATH | self::FILTER_BY_NAME))) {\n            $mode |= self::FILTER_BY_PATH;\n        }\n\n        if (!($mode & (self::MATCH_PREFIX | self::MATCH_SUFFIX | self::MATCH_REGEX))) {\n            $mode |= self::MATCH_REGEX;\n        }\n\n        $this->pattern = $pattern;\n        $this->patternLength = strlen($pattern);\n        $this->mode = $mode;\n    }\n\n    /**\n     * Returns whether the current element should be accepted.\n     *\n     * @return bool|int Returns `false` if the current element should be filtered out.\n     */\n    public function accept()\n    {\n        if ($this->mode & self::FILTER_BY_PATH) {\n            $value = $this->getCurrentResource()->getPath();\n        } else {\n            $value = $this->getCurrentResource()->getName();\n        }\n\n        if ($this->mode & self::MATCH_PREFIX) {\n            return 0 === strpos($value, $this->pattern);\n        } elseif ($this->mode & self::MATCH_SUFFIX) {\n            return $this->pattern === substr($value, -$this->patternLength);\n        } else {\n            return preg_match($this->pattern, $value);\n        }\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getCurrentResource()\n    {\n        return $this->getInnerIterator()->getCurrentResource();\n    }\n}\n"
  },
  {
    "path": "src/Resource/LinkResource.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Resource;\n\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\nuse Puli\\Repository\\Api\\ResourceNotFoundException;\nuse Puli\\Repository\\Resource\\Collection\\ArrayResourceCollection;\n\n/**\n * A link resource targeting to another resource.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n * @author Titouan Galopin <galopintitouan@gmail.com>\n */\nclass LinkResource extends GenericResource implements PuliResource\n{\n    /**\n     * @var string\n     */\n    private $targetPath;\n\n    /**\n     * @param string      $targetPath\n     * @param string|null $path\n     */\n    public function __construct($targetPath, $path = null)\n    {\n        parent::__construct($path);\n\n        $this->targetPath = $targetPath;\n    }\n\n    /**\n     * @return string\n     */\n    public function getTargetPath()\n    {\n        return $this->targetPath;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getTarget()\n    {\n        if (!$this->getRepository()) {\n            throw ResourceNotFoundException::forPath($this->getTargetPath());\n        }\n\n        return $this->getRepository()->get($this->getTargetPath());\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getChild($relPath)\n    {\n        if (!$this->getRepository()) {\n            throw ResourceNotFoundException::forPath($this->getTargetPath().'/'.$relPath);\n        }\n\n        return $this->getRepository()->get($this->getTargetPath().'/'.$relPath);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function hasChild($relPath)\n    {\n        if (!$this->getRepository()) {\n            return false;\n        }\n\n        return $this->getRepository()->contains($this->getTargetPath().'/'.$relPath);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function hasChildren()\n    {\n        if (!$this->getRepository()) {\n            return false;\n        }\n\n        return $this->getRepository()->hasChildren($this->getRepositoryPath());\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function listChildren()\n    {\n        $children = new ArrayResourceCollection();\n\n        if (!$this->getRepository()) {\n            return $children;\n        }\n\n        foreach ($this->getRepository()->listChildren($this->getTargetPath()) as $child) {\n            $children[$child->getName()] = $child;\n        }\n\n        return $children;\n    }\n\n    protected function preSerialize(array &$data)\n    {\n        parent::preSerialize($data);\n\n        $data[] = $this->targetPath;\n    }\n\n    protected function postUnserialize(array $data)\n    {\n        $this->targetPath = array_pop($data);\n\n        parent::postUnserialize($data);\n    }\n}\n"
  },
  {
    "path": "src/Resource/Metadata/FilesystemMetadata.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Resource\\Metadata;\n\nuse Puli\\Repository\\Api\\Resource\\ResourceMetadata;\n\n/**\n * Metadata about a file on the filesystem.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass FilesystemMetadata extends ResourceMetadata\n{\n    private $filesystemPath;\n\n    public function __construct($filesystemPath)\n    {\n        $this->filesystemPath = $filesystemPath;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getCreationTime()\n    {\n        if (defined('PHP_WINDOWS_VERSION_MAJOR')) {\n            $path = $this->fixWindowsPath($this->filesystemPath);\n            clearstatcache(true, $path);\n\n            return filectime($path);\n        }\n\n        // On Unix, filectime() returns the change time of the inode, not the\n        // creation time.\n        return 0;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getAccessTime()\n    {\n        $path = $this->fixWindowsPath($this->filesystemPath);\n        clearstatcache(true, $path);\n\n        return fileatime($path);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getModificationTime()\n    {\n        $path = $this->fixWindowsPath($this->filesystemPath);\n        clearstatcache(true, $path);\n\n        return filemtime($path);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getSize()\n    {\n        $path = $this->fixWindowsPath($this->filesystemPath);\n        clearstatcache(true, $path);\n\n        return filesize($path);\n    }\n\n    /**\n     * On Windows, fileXtime functions see only changes\n     * on the symlink file and not the original one.\n     *\n     * @param string $path\n     *\n     * @return string\n     */\n    private function fixWindowsPath($path)\n    {\n        if (is_link($path) && defined('PHP_WINDOWS_VERSION_MAJOR')) {\n            $path = readlink($path);\n        }\n\n        return $path;\n    }\n}\n"
  },
  {
    "path": "src/StreamWrapper/ResourceStreamWrapper.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\StreamWrapper;\n\nuse InvalidArgumentException;\nuse Puli\\Repository\\Api\\Resource\\BodyResource;\nuse Puli\\Repository\\Api\\Resource\\FilesystemResource;\nuse Puli\\Repository\\Api\\ResourceNotFoundException;\nuse Puli\\Repository\\Api\\ResourceRepository;\nuse Puli\\Repository\\Api\\UnsupportedOperationException;\nuse Puli\\Repository\\Api\\UnsupportedResourceException;\nuse Puli\\Repository\\RepositoryFactoryException;\nuse Puli\\Repository\\Resource\\Iterator\\ResourceCollectionIterator;\nuse Puli\\Repository\\Uri\\Uri;\nuse Webmozart\\Assert\\Assert;\n\n/**\n * Registers a PHP stream wrapper for a {@link ResourceRepository}.\n *\n * To register the stream wrapper, call {@link register}:\n *\n * ```php\n * use Puli\\Repository\\InMemoryRepository;\n * use Puli\\Repository\\StreamWrapper\\ResourceStreamWrapper;\n *\n * $repo = new InMemoryRepository();\n *\n * ResourceStreamWrapper::register('puli', $repo);\n *\n * file_get_contents('puli:///css/style.css');\n * // => $puliRepo->get('/css/style.css')->getBody()\n * ```\n *\n * The stream wrapper can only be used for reading, not writing.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass ResourceStreamWrapper implements StreamWrapper\n{\n    const DEVICE_ASSOC = 'dev';\n\n    const DEVICE_NUM = 0;\n\n    const INODE_ASSOC = 'ino';\n\n    const INODE_NUM = 1;\n\n    const MODE_ASSOC = 'mode';\n\n    const MODE_NUM = 2;\n\n    const NUM_LINKS_ASSOC = 'nlink';\n\n    const NUM_LINK_NUM = 3;\n\n    const UID_ASSOC = 'uid';\n\n    const UID_NUM = 4;\n\n    const GID_ASSOC = 'gid';\n\n    const GID_NUM = 5;\n\n    const DEVICE_TYPE_ASSOC = 'rdev';\n\n    const DEVICE_TYPE_NUM = 6;\n\n    const SIZE_ASSOC = 'size';\n\n    const SIZE_NUM = 7;\n\n    const ACCESS_TIME_ASSOC = 'atime';\n\n    const ACCESS_TIME_NUM = 8;\n\n    const MODIFY_TIME_ASSOC = 'mtime';\n\n    const MODIFY_TIME_NUM = 9;\n\n    const CHANGE_TIME_ASSOC = 'ctime';\n\n    const CHANGE_TIME_NUM = 10;\n\n    const BLOCK_SIZE_ASSOC = 'blksize';\n\n    const BLOCK_SIZE_NUM = 11;\n\n    const NUM_BLOCKS_ASSOC = 'blocks';\n\n    const NUM_BLOCKS_NUM = 12;\n\n    /**\n     * @var array\n     */\n    private static $defaultStat = array(\n        self::DEVICE_ASSOC => -1,\n        self::DEVICE_NUM => -1,\n        self::INODE_ASSOC => -1,\n        self::INODE_NUM => -1,\n        self::MODE_ASSOC => -1,\n        self::MODE_NUM => -1,\n        self::NUM_LINKS_ASSOC => -1,\n        self::NUM_LINK_NUM => -1,\n        self::UID_ASSOC => 0,\n        self::UID_NUM => 0,\n        self::GID_ASSOC => 0,\n        self::GID_NUM => 0,\n        self::DEVICE_TYPE_ASSOC => -1,\n        self::DEVICE_TYPE_NUM => -1,\n        self::SIZE_ASSOC => 0,\n        self::SIZE_NUM => 0,\n        self::ACCESS_TIME_ASSOC => -1,\n        self::ACCESS_TIME_NUM => -1,\n        self::MODIFY_TIME_ASSOC => -1,\n        self::MODIFY_TIME_NUM => -1,\n        self::CHANGE_TIME_ASSOC => -1,\n        self::CHANGE_TIME_NUM => -1,\n        self::BLOCK_SIZE_ASSOC => -1,\n        self::BLOCK_SIZE_NUM => -1,\n        self::NUM_BLOCKS_ASSOC => 0,\n        self::NUM_BLOCKS_NUM => 0,\n    );\n\n    /**\n     * @var ResourceRepository[]|callable[]\n     */\n    private static $repos;\n\n    /**\n     * @var resource\n     */\n    private $handle;\n\n    /**\n     * @var ResourceCollectionIterator\n     */\n    private $childIterator;\n\n    /**\n     * Registers a repository as PHP stream wrapper.\n     *\n     * The resources of the repository can subsequently be accessed with PHP's\n     * file system by prefixing the resource paths with the registered URI\n     * scheme:\n     *\n     * ```php\n     * ResourceStreamWrapper::register('puli', $repo);\n     *\n     * // /app/css/style.css\n     * $contents = file_get_contents('puli:///app/css/style.css');\n     * ```\n     *\n     * Instead of passing a repository, you can also pass a callable. The\n     * callable is executed when the repository is accessed for the first time\n     * and should return a valid {@link ResourceRepository} instance.\n     *\n     * @param string                      $scheme            The URI scheme.\n     * @param ResourceRepository|callable $repositoryFactory The repository to use.\n     *\n     * @throws StreamWrapperException If a repository was previously registered\n     *                                for the same scheme. Call\n     *                                {@link unregister()} to unregister the\n     *                                scheme first.\n     */\n    public static function register($scheme, $repositoryFactory)\n    {\n        if (!$repositoryFactory instanceof ResourceRepository\n                && !is_callable($repositoryFactory)) {\n            throw new InvalidArgumentException(sprintf(\n                'The repository factory should be a callable or an instance '.\n                'of ResourceRepository. Got: %s',\n                $repositoryFactory\n            ));\n        }\n\n        Assert::string($scheme, 'The scheme must be a string. Got: %s');\n        Assert::alnum($scheme, 'The scheme %s should consist of letters and digits only.');\n        Assert::startsWithLetter($scheme, 'The scheme %s should start with a letter.');\n\n        if (isset(self::$repos[$scheme])) {\n            throw new StreamWrapperException(sprintf(\n                'The scheme \"%s\" has already been registered.',\n                $scheme\n            ));\n        }\n\n        self::$repos[$scheme] = $repositoryFactory;\n\n        stream_wrapper_register($scheme, __CLASS__);\n    }\n\n    /**\n     * Unregisters the given scheme.\n     *\n     * Unknown schemes are ignored.\n     *\n     * @param string $scheme A URI scheme.\n     */\n    public static function unregister($scheme)\n    {\n        if (!isset(self::$repos[$scheme])) {\n            return;\n        }\n\n        unset(self::$repos[$scheme]);\n\n        stream_wrapper_unregister($scheme);\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @internal\n     */\n    public function dir_opendir($uri, $options)\n    {\n        $parts = Uri::parse($uri);\n\n        // Provoke ResourceNotFoundException if not found\n        $resource = $this->getRepository($parts['scheme'])->get($parts['path']);\n\n        $this->childIterator = new ResourceCollectionIterator(\n            $resource->listChildren(),\n            ResourceCollectionIterator::CURRENT_AS_NAME\n        );\n\n        return true;\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @internal\n     */\n    public function dir_closedir()\n    {\n        $this->childIterator = null;\n\n        return false;\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @internal\n     */\n    public function dir_readdir()\n    {\n        if (!$this->childIterator->valid()) {\n            return false;\n        }\n\n        $name = $this->childIterator->current();\n\n        $this->childIterator->next();\n\n        return $name;\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @internal\n     */\n    public function dir_rewinddir()\n    {\n        $this->childIterator->rewind();\n\n        return true;\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @internal\n     */\n    public function mkdir($uri, $mode, $options)\n    {\n        throw new UnsupportedOperationException(sprintf(\n            'The creation of new directories through the stream wrapper is '.\n            'not supported. Tried to create the directory \"%s\".',\n            $uri\n        ));\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @internal\n     */\n    public function rename($uriFrom, $uriTo)\n    {\n        $parts = Uri::parse($uriFrom);\n\n        // validate whether the URL exists\n        $this->getRepository($parts['scheme'])->get($parts['path']);\n\n        throw new UnsupportedOperationException(sprintf(\n            'The renaming of resources through the stream wrapper is not '.\n            'supported. Tried to rename \"%s\" to \"%s\".',\n            $uriFrom,\n            $uriTo\n        ));\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @internal\n     */\n    public function rmdir($uri, $options)\n    {\n        $parts = Uri::parse($uri);\n\n        // validate whether the URL exists\n        $resource = $this->getRepository($parts['scheme'])->get($parts['path']);\n\n        throw new UnsupportedOperationException(sprintf(\n            'The removal of directories through the stream wrapper is not '.\n            'supported. Tried to remove \"%s\"%s.',\n            $uri,\n            $resource instanceof FilesystemResource\n                ? sprintf(' which points to \"%s\"', $resource->getFilesystemPath())\n                : ''\n        ));\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @internal\n     */\n    public function stream_cast($castAs)\n    {\n        return $this->handle;\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @internal\n     */\n    public function stream_close()\n    {\n        assert(null !== $this->handle);\n\n        return fclose($this->handle);\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @internal\n     */\n    public function stream_eof()\n    {\n        assert(null !== $this->handle);\n\n        return feof($this->handle);\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @internal\n     */\n    public function stream_flush()\n    {\n        assert(null !== $this->handle);\n\n        return fflush($this->handle);\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @internal\n     */\n    public function stream_lock($operation)\n    {\n        throw new UnsupportedOperationException(\n            'The locking of files through the stream wrapper is not '.\n            'supported.'\n        );\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @internal\n     */\n    public function stream_metadata($uri, $option)\n    {\n        switch ($option) {\n            case STREAM_META_TOUCH:\n                throw new UnsupportedOperationException(sprintf(\n                    'Touching files through the stream wrapper is not '.\n                    'supported. Tried to touch \"%s\".',\n                    $uri\n                ));\n\n            case STREAM_META_OWNER:\n            case STREAM_META_OWNER_NAME:\n                throw new UnsupportedOperationException(sprintf(\n                    'Changing file ownership through the stream wrapper '.\n                    'is not supported. Tried to chown \"%s\".',\n                    $uri\n                ));\n\n            case STREAM_META_GROUP:\n            case STREAM_META_GROUP_NAME:\n                throw new UnsupportedOperationException(sprintf(\n                    'Changing file groups through the stream wrapper '.\n                    'is not supported. Tried to chgrp \"%s\".',\n                    $uri\n                ));\n\n            case STREAM_META_ACCESS:\n                throw new UnsupportedOperationException(sprintf(\n                    'Changing file permissions through the stream wrapper '.\n                    'is not supported. Tried to chmod \"%s\".',\n                    $uri\n                ));\n        }\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @internal\n     */\n    public function stream_open($uri, $mode, $options, &$openedPath)\n    {\n        if (!preg_match('/^[rbt]+$/', $mode)) {\n            throw new UnsupportedOperationException(sprintf(\n                'Resources can only be opened for reading. Tried to open \"%s\" '.\n                'with mode \"%s\".',\n                $uri,\n                $mode\n            ));\n        }\n\n        $parts = Uri::parse($uri);\n\n        $resource = $this->getRepository($parts['scheme'])->get($parts['path']);\n\n        if (!$resource instanceof BodyResource) {\n            throw new UnsupportedResourceException(sprintf(\n                'Can only open file resources for reading. Tried to open \"%s\" '.\n                'of type %s which does not implement BodyResource.',\n                $uri,\n                get_class($resource)\n            ));\n        }\n\n        if ($resource instanceof FilesystemResource) {\n            $this->handle = fopen($resource->getFilesystemPath(), 'r', $options & STREAM_USE_PATH) ?: null;\n\n            return null !== $this->handle;\n        }\n\n        $this->handle = fopen('php://temp', 'r+', $options & STREAM_USE_PATH);\n        fwrite($this->handle, $resource->getBody());\n        rewind($this->handle);\n\n        return true;\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @internal\n     */\n    public function stream_read($length)\n    {\n        assert(null !== $this->handle);\n\n        return fread($this->handle, $length);\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @internal\n     */\n    public function stream_seek($offset, $whence = SEEK_SET)\n    {\n        assert(null !== $this->handle);\n\n        return 0 === fseek($this->handle, $offset, $whence);\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @internal\n     */\n    public function stream_set_option($option, $arg1, $arg2)\n    {\n        // noop\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @internal\n     */\n    public function stream_stat()\n    {\n        assert(null !== $this->handle);\n\n        return fstat($this->handle);\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @internal\n     */\n    public function stream_tell()\n    {\n        assert(null !== $this->handle);\n\n        return ftell($this->handle);\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @internal\n     */\n    public function stream_truncate($newSize)\n    {\n        assert(null !== $this->handle);\n\n        return ftruncate($this->handle, $newSize);\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @internal\n     */\n    public function stream_write($data)\n    {\n        assert(null !== $this->handle);\n\n        return fwrite($this->handle, $data);\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @internal\n     */\n    public function unlink($uri)\n    {\n        throw new UnsupportedOperationException(sprintf(\n            'The removal of files through the stream wrapper is not '.\n            'supported. Tried to remove \"%s\".',\n            $uri\n        ));\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @internal\n     */\n    public function url_stat($uri, $flags)\n    {\n        try {\n            $parts = Uri::parse($uri);\n\n            $resource = $this->getRepository($parts['scheme'])->get($parts['path']);\n\n            if ($resource instanceof FilesystemResource) {\n                $path = $resource->getFilesystemPath();\n\n                if ($flags & STREAM_URL_STAT_LINK) {\n                    return lstat($path);\n                }\n\n                return stat($path);\n            }\n\n            $stat = self::$defaultStat;\n\n            $metadata = $resource->getMetadata();\n\n            $stat[self::SIZE_NUM] = $stat[self::SIZE_ASSOC] = $metadata->getSize();\n            $stat[self::ACCESS_TIME_NUM] = $stat[self::ACCESS_TIME_ASSOC] = $metadata->getAccessTime();\n            $stat[self::MODIFY_TIME_NUM] = $stat[self::MODIFY_TIME_ASSOC] = $metadata->getModificationTime();\n\n            return $stat;\n        } catch (ResourceNotFoundException $e) {\n            if ($flags & STREAM_URL_STAT_QUIET) {\n                // Same result as stat() returns on error\n                // file_exists() returns false for this resource\n                return false;\n            }\n\n            throw $e;\n        }\n    }\n\n    /**\n     * Constructs (if necessary) and returns the repository for the given scheme.\n     *\n     * @param string $scheme A URI scheme.\n     *\n     * @return ResourceRepository The resource repository.\n     *\n     * @throws RepositoryFactoryException If the callable did not return an\n     *                                    instance of {@link ResourceRepository}.\n     * @throws StreamWrapperException     If the scheme is not supported.\n     */\n    private function getRepository($scheme)\n    {\n        if (!isset(self::$repos[$scheme])) {\n            throw new StreamWrapperException(sprintf(\n                'The stream wrapper has not been registered for the scheme \"%s\". '.\n                'Please call ResourceStreamWrapper::register() first.',\n                $scheme\n            ));\n        }\n\n        if (is_callable(self::$repos[$scheme])) {\n            $callable = self::$repos[$scheme];\n            $result = $callable($scheme);\n\n            if (!$result instanceof ResourceRepository) {\n                throw new RepositoryFactoryException(sprintf(\n                    'The repository factory registered for scheme \"%s\" should '.\n                    'return a ResourceRepository instance. Got: %s',\n                    $scheme,\n                    is_object($result) ? get_class($result) : gettype($result)\n                ));\n            }\n\n            self::$repos[$scheme] = $result;\n        }\n\n        return self::$repos[$scheme];\n    }\n}\n"
  },
  {
    "path": "src/StreamWrapper/StreamWrapper.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\StreamWrapper;\n\n/**\n * API of stream wrappers as supported by PHP.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n *\n * @see    http://php.net/manual/en/class.streamwrapper.php\n */\ninterface StreamWrapper\n{\n    public function dir_closedir();\n\n    public function dir_opendir($url, $options);\n\n    public function dir_readdir();\n\n    public function dir_rewinddir();\n\n    public function mkdir($url, $mode, $options);\n\n    public function rename($urlFrom, $urlTo);\n\n    public function rmdir($url, $options);\n\n    public function stream_cast($castAs);\n\n    public function stream_close();\n\n    public function stream_eof();\n\n    public function stream_flush();\n\n    public function stream_lock($operation);\n\n    public function stream_open($url, $mode, $options, &$openedPath);\n\n    public function stream_read($length);\n\n    public function stream_seek($offset, $whence = SEEK_SET);\n\n    public function stream_set_option($option, $arg1, $arg2);\n\n    public function stream_stat();\n\n    public function stream_tell();\n\n    public function stream_write($data);\n\n    public function unlink($url);\n\n    public function url_stat($url, $flags);\n}\n"
  },
  {
    "path": "src/StreamWrapper/StreamWrapperException.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\StreamWrapper;\n\nuse RuntimeException;\n\n/**\n * Thrown when the stream wrapper was not registered or is registered twice.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass StreamWrapperException extends RuntimeException\n{\n}\n"
  },
  {
    "path": "src/Uri/InvalidUriException.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Uri;\n\nuse RuntimeException;\n\n/**\n * Thrown when a URI is invalid.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass InvalidUriException extends RuntimeException\n{\n}\n"
  },
  {
    "path": "src/Uri/Uri.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Uri;\n\n/**\n * Utility methods for handling URIs.\n *\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nfinal class Uri\n{\n    /**\n     * Parses a URI.\n     *\n     * The returned array contains the following keys:\n     *\n     *  * \"scheme\": The scheme part before the \"://\";\n     *  * \"path\": The path part after the \"://\".\n     *\n     * The URI must fulfill a few constraints:\n     *\n     *  * the scheme must consist of alphabetic characters only;\n     *  * the scheme may be omitted. Then \"://\" must be omitted too;\n     *  * the path must not be empty;\n     *  * the path must start with a forward slash (\"/\").\n     *\n     * If any of these constraints is not fulfilled, an\n     * {@link InvalidUriException} is thrown.\n     *\n     * @param string $uri A URI string.\n     *\n     * @return string[] The parts of the URI.\n     *\n     * @throws InvalidUriException If the URI is invalid.\n     */\n    public static function parse($uri)\n    {\n        if (!is_string($uri)) {\n            throw new InvalidUriException(sprintf(\n                'The URI must be a string, but is a %s.',\n                is_object($uri) ? get_class($uri) : gettype($uri)\n            ));\n        }\n\n        if (false !== ($pos = strpos($uri, '://'))) {\n            $parts = array(substr($uri, 0, $pos), substr($uri, $pos + 3));\n\n            if (!ctype_alnum($parts[0])) {\n                throw new InvalidUriException(sprintf(\n                    'The URI \"%s\" is invalid. The scheme should consist of '.\n                    'alphabetic characters only.',\n                    $uri\n                ));\n            }\n\n            if (!ctype_alpha($parts[0][0])) {\n                throw new InvalidUriException(sprintf(\n                    'The URI \"%s\" is invalid. The scheme should start with a letter.',\n                    $uri\n                ));\n            }\n        } else {\n            $parts = array('', $uri);\n        }\n\n        if ('' === $parts[1]) {\n            throw new InvalidUriException(sprintf(\n                'The URI \"%s\" is invalid. The path should not be empty.',\n                $uri\n            ));\n        }\n\n        if ('/' !== $parts[1][0]) {\n            throw new InvalidUriException(sprintf(\n                'The URI \"%s\" is invalid. The path should start with a '.\n                'forward slash (\"/\").',\n                $uri\n            ));\n        }\n\n        return array(\n            'scheme' => $parts[0],\n            'path' => $parts[1],\n        );\n    }\n\n    private function __construct()\n    {\n    }\n}\n"
  },
  {
    "path": "tests/AbstractEditableRepositoryTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests;\n\nuse Puli\\Repository\\Api\\EditableRepository;\nuse Puli\\Repository\\ChangeStream\\InMemoryChangeStream;\nuse Puli\\Repository\\InMemoryRepository;\nuse Puli\\Repository\\Resource\\Collection\\ArrayResourceCollection;\nuse Puli\\Repository\\Resource\\DirectoryResource;\nuse Puli\\Repository\\Resource\\FileResource;\nuse Puli\\Repository\\Resource\\LinkResource;\nuse Puli\\Repository\\Tests\\Resource\\TestDirectory;\nuse Puli\\Repository\\Tests\\Resource\\TestFile;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nabstract class AbstractEditableRepositoryTest extends AbstractRepositoryTest\n{\n    /**\n     * The instance used to change the contents of the repository.\n     *\n     * @var EditableRepository\n     */\n    protected $writeRepo;\n\n    /**\n     * The instance used to check the changed contents of the repository.\n     *\n     * This is either the same instance as {@link $writeRepo} or a different\n     * instance that uses the same data source.\n     *\n     * @var EditableRepository\n     */\n    protected $readRepo;\n\n    /**\n     * @var InMemoryChangeStream\n     */\n    protected $stream;\n\n    private static $symlinkOnWindows = null;\n\n    public static function setUpBeforeClass()\n    {\n        // Detect whether symlinks are supported on Windows (it requires enough privileges)\n        // This logic is copied from the Symfony Filesystem component testsuite\n        if ('\\\\' === DIRECTORY_SEPARATOR && null === self::$symlinkOnWindows) {\n            $target = tempnam(sys_get_temp_dir(), 'sl');\n            $link = sys_get_temp_dir().'/sl'.microtime(true).mt_rand();\n            if (self::$symlinkOnWindows = @symlink($target, $link)) {\n                unlink($link);\n            }\n            unlink($target);\n        }\n    }\n\n    /**\n     * @return EditableRepository\n     */\n    abstract protected function createWriteRepository();\n\n    /**\n     * @param EditableRepository $writeRepo\n     *\n     * @return EditableRepository\n     */\n    abstract protected function createReadRepository(EditableRepository $writeRepo);\n\n    protected function setUp()\n    {\n        parent::setUp();\n\n        $this->stream = new InMemoryChangeStream();\n        $this->writeRepo = $this->createWriteRepository();\n        $this->readRepo = $this->createReadRepository($this->writeRepo);\n    }\n\n    protected function markAsSkippedIfSymlinkIsMissing()\n    {\n        if (!function_exists('symlink')) {\n            $this->markTestSkipped('symlink is not supported');\n        }\n\n        if ('\\\\' === DIRECTORY_SEPARATOR && false === self::$symlinkOnWindows) {\n            $this->markTestSkipped('symlink requires \"Create symbolic links\" privilege on windows');\n        }\n    }\n\n    public function testRootIsEmptyBeforeAdding()\n    {\n        $root = $this->readRepo->get('/');\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\PuliResource', $root);\n        $this->assertCount(0, $root->listChildren());\n        $this->assertSame('/', $root->getPath());\n    }\n\n    public function testAddFile()\n    {\n        $this->writeRepo->add('/webmozart/puli', $this->prepareFixtures($this->createDirectory()));\n        $this->writeRepo->add('/webmozart/puli/file', $this->prepareFixtures($this->createFile()));\n\n        $dir = $this->readRepo->get('/webmozart/puli');\n        $file = $this->readRepo->get('/webmozart/puli/file');\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\PuliResource', $dir);\n        $this->assertSame('/webmozart/puli', $dir->getPath());\n        $this->assertSame($this->readRepo, $dir->getRepository());\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $file);\n        $this->assertSame('/webmozart/puli/file', $file->getPath());\n        $this->assertSame($this->readRepo, $file->getRepository());\n        $this->assertSame(TestFile::BODY, $file->getBody());\n    }\n\n    public function testAddDoesNotAttachResourceToRepository()\n    {\n        $directory = $this->prepareFixtures($this->createDirectory('/dir', array(\n            $this->createFile('/file1'),\n            $this->createFile('/file2'),\n        )));\n        $file1 = $directory->getChild('file1');\n        $file2 = $directory->getChild('file2');\n\n        $this->writeRepo->add('/webmozart', $directory);\n\n        $this->assertNull($directory->getRepository());\n        $this->assertNull($file1->getRepository());\n        $this->assertNull($file2->getRepository());\n\n        $directory = $this->readRepo->get('/webmozart');\n        $file1 = $this->readRepo->get('/webmozart/file1');\n        $file2 = $this->readRepo->get('/webmozart/file2');\n\n        $this->assertSame($this->readRepo, $directory->getRepository());\n        $this->assertSame($this->readRepo, $file1->getRepository());\n        $this->assertSame($this->readRepo, $file2->getRepository());\n    }\n\n    public function testAddDoesNotChangeAttachedRepository()\n    {\n        $otherRepo = new InMemoryRepository();\n        $otherRepo->add('/dir', $this->prepareFixtures($this->createDirectory('/', array(\n            $this->createFile('/file1'),\n            $this->createFile('/file2'),\n        ))));\n\n        $directory = $otherRepo->get('/dir');\n        $file1 = $otherRepo->get('/dir/file1');\n        $file2 = $otherRepo->get('/dir/file2');\n\n        $this->writeRepo->add('/webmozart', $directory);\n\n        $this->assertSame($otherRepo, $directory->getRepository());\n        $this->assertSame($otherRepo, $file1->getRepository());\n        $this->assertSame($otherRepo, $file2->getRepository());\n\n        $directory = $this->readRepo->get('/webmozart');\n        $file1 = $this->readRepo->get('/webmozart/file1');\n        $file2 = $this->readRepo->get('/webmozart/file2');\n\n        $this->assertSame($this->readRepo, $directory->getRepository());\n        $this->assertSame($this->readRepo, $file1->getRepository());\n        $this->assertSame($this->readRepo, $file2->getRepository());\n    }\n\n    public function testAddMergesResourceChildren()\n    {\n        $this->writeRepo->add('/webmozart/puli', $this->prepareFixtures($this->createDirectory('/foo', array(\n            $this->createFile('/file1', 'original 1'),\n            $this->createFile('/file2', 'original 2'),\n        ))));\n\n        $this->writeRepo->add('/webmozart/puli', $this->prepareFixtures($this->createDirectory('/bar', array(\n            $this->createFile('/file1', 'override 1'),\n            $this->createFile('/file3', 'override 3'),\n        ))));\n\n        $dir = $this->readRepo->get('/webmozart/puli');\n        $file1 = $this->readRepo->get('/webmozart/puli/file1');\n        $file2 = $this->readRepo->get('/webmozart/puli/file2');\n        $file3 = $this->readRepo->get('/webmozart/puli/file3');\n\n        $this->assertTrue($this->readRepo->hasChildren('/webmozart/puli'));\n        $this->assertCount(3, $this->readRepo->listChildren('/webmozart/puli'));\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\PuliResource', $dir);\n        $this->assertSame('/webmozart/puli', $dir->getPath());\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $file1);\n        $this->assertSame('/webmozart/puli/file1', $file1->getPath());\n        $this->assertSame('override 1', $file1->getBody());\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $file2);\n        $this->assertSame('/webmozart/puli/file2', $file2->getPath());\n        $this->assertSame('original 2', $file2->getBody());\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $file3);\n        $this->assertSame('/webmozart/puli/file3', $file3->getPath());\n        $this->assertSame('override 3', $file3->getBody());\n    }\n\n    public function testAddDot()\n    {\n        $this->writeRepo->add('/webmozart/puli/file/.', $this->prepareFixtures($this->createFile()));\n\n        $file = $this->readRepo->get('/webmozart/puli/file');\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $file);\n        $this->assertSame('/webmozart/puli/file', $file->getPath());\n    }\n\n    public function testAddDotDot()\n    {\n        $this->writeRepo->add('/webmozart/puli/file/..', $this->prepareFixtures($this->createFile()));\n\n        $file = $this->readRepo->get('/webmozart/puli');\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $file);\n        $this->assertSame('/webmozart/puli', $file->getPath());\n    }\n\n    public function testAddTrimsTrailingSlash()\n    {\n        $this->writeRepo->add('/webmozart/puli/file/', $this->prepareFixtures($this->createFile()));\n\n        $file = $this->readRepo->get('/webmozart/puli/file');\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $file);\n        $this->assertSame('/webmozart/puli/file', $file->getPath());\n    }\n\n    public function testAddCollection()\n    {\n        $this->writeRepo->add('/webmozart/puli', new ArrayResourceCollection(array(\n            $this->prepareFixtures($this->createFile('/file1')),\n            $this->prepareFixtures($this->createFile('/file2')),\n        )));\n\n        $file1 = $this->readRepo->get('/webmozart/puli/file1');\n        $file2 = $this->readRepo->get('/webmozart/puli/file2');\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $file1);\n        $this->assertSame('/webmozart/puli/file1', $file1->getPath());\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $file2);\n        $this->assertSame('/webmozart/puli/file2', $file2->getPath());\n    }\n\n    public function testAddRoot()\n    {\n        $this->writeRepo->add('/', $this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart', array(\n                $this->createFile('/file'),\n            )),\n        ))));\n\n        $root = $this->readRepo->get('/');\n        $dir = $this->readRepo->get('/webmozart');\n        $file = $this->readRepo->get('/webmozart/file');\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\PuliResource', $root);\n        $this->assertSame('/', $root->getPath());\n        $this->assertSame($this->readRepo, $root->getRepository());\n        $this->assertCount(1, $root->listChildren());\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\PuliResource', $dir);\n        $this->assertSame('/webmozart', $dir->getPath());\n        $this->assertSame($this->readRepo, $dir->getRepository());\n        $this->assertCount(1, $dir->listChildren());\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $file);\n        $this->assertSame('/webmozart/file', $file->getPath());\n        $this->assertSame($this->readRepo, $file->getRepository());\n        $this->assertSame(TestFile::BODY, $file->getBody());\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testAddExpectsAbsolutePath()\n    {\n        $this->writeRepo->add('webmozart', $this->prepareFixtures($this->createDirectory()));\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testAddExpectsNonEmptyPath()\n    {\n        $this->writeRepo->add('', $this->prepareFixtures($this->createDirectory()));\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testAddExpectsStringPath()\n    {\n        $this->writeRepo->add(new \\stdClass(), $this->prepareFixtures($this->createDirectory()));\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedResourceException\n     */\n    public function testAddExpectsResource()\n    {\n        $this->writeRepo->add('/webmozart', new \\stdClass());\n    }\n\n    public function testOverride()\n    {\n        $this->writeRepo->add('/webmozart/file', $this->prepareFixtures($this->createFile('/file1', 'BODY0')));\n        $this->writeRepo->add('/webmozart/file', $this->prepareFixtures($this->createFile('/file2', 'BODY1')));\n        $this->writeRepo->add('/webmozart/file', $this->prepareFixtures($this->createFile('/file3', 'BODY2')));\n\n        $versions = $this->readRepo->getVersions('/webmozart/file');\n\n        $this->assertSame(array(0, 1, 2), $versions->getVersions());\n\n        $v0 = $versions->get(0);\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $v0);\n        $this->assertSame('/webmozart/file', $v0->getPath());\n        $this->assertSame($this->readRepo, $v0->getRepository());\n        $this->assertSame('BODY0', $v0->getBody());\n\n        $v1 = $versions->get(1);\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $v1);\n        $this->assertSame('/webmozart/file', $v1->getPath());\n        $this->assertSame($this->readRepo, $v1->getRepository());\n        $this->assertSame('BODY1', $v1->getBody());\n\n        $v2 = $versions->get(2);\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $v2);\n        $this->assertSame('/webmozart/file', $v2->getPath());\n        $this->assertSame($this->readRepo, $v2->getRepository());\n        $this->assertSame('BODY2', $v2->getBody());\n    }\n\n    public function testOverrideSuperPath()\n    {\n        $this->writeRepo->add('/webmozart/puli/file', $this->prepareFixtures($this->createFile('/file', 'BODY0')));\n\n        $this->writeRepo->add('/webmozart/puli', $this->prepareFixtures($this->createDirectory('/dir1', array(\n            $this->createFile('/file', 'BODY1'),\n        ))));\n\n        $this->writeRepo->add('/webmozart', $this->prepareFixtures($this->createDirectory('/dir2', array(\n            $this->createDirectory('/puli', array(\n                $this->createFile('/file', 'BODY2'),\n            )),\n        ))));\n\n        $versions = $this->readRepo->getVersions('/webmozart/puli/file');\n\n        $this->assertSame(array(0, 1, 2), $versions->getVersions());\n\n        $v0 = $versions->get(0);\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $v0);\n        $this->assertSame('/webmozart/puli/file', $v0->getPath());\n        $this->assertSame($this->readRepo, $v0->getRepository());\n        $this->assertSame('BODY0', $v0->getBody());\n\n        $v1 = $versions->get(1);\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $v1);\n        $this->assertSame('/webmozart/puli/file', $v1->getPath());\n        $this->assertSame($this->readRepo, $v1->getRepository());\n        $this->assertSame('BODY1', $v1->getBody());\n\n        $v2 = $versions->get(2);\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $v2);\n        $this->assertSame('/webmozart/puli/file', $v2->getPath());\n        $this->assertSame($this->readRepo, $v2->getRepository());\n        $this->assertSame('BODY2', $v2->getBody());\n    }\n\n    public function testOverrideSubPath()\n    {\n        $this->writeRepo->add('/webmozart', $this->prepareFixtures($this->createDirectory('/dir1', array(\n            $this->createDirectory('/puli', array(\n                $this->createFile('/file', 'BODY0'),\n            )),\n        ))));\n\n        $this->writeRepo->add('/webmozart/puli', $this->prepareFixtures($this->createDirectory('/dir2', array(\n            $this->createFile('/file', 'BODY1'),\n        ))));\n\n        $this->writeRepo->add('/webmozart/puli/file', $this->prepareFixtures($this->createFile('/file', 'BODY2')));\n\n        $versions = $this->readRepo->getVersions('/webmozart/puli/file');\n\n        $this->assertSame(array(0, 1, 2), $versions->getVersions());\n\n        $v0 = $versions->get(0);\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $v0);\n        $this->assertSame('/webmozart/puli/file', $v0->getPath());\n        $this->assertSame($this->readRepo, $v0->getRepository());\n        $this->assertSame('BODY0', $v0->getBody());\n\n        $v1 = $versions->get(1);\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $v1);\n        $this->assertSame('/webmozart/puli/file', $v1->getPath());\n        $this->assertSame($this->readRepo, $v1->getRepository());\n        $this->assertSame('BODY1', $v1->getBody());\n\n        $v2 = $versions->get(2);\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $v2);\n        $this->assertSame('/webmozart/puli/file', $v2->getPath());\n        $this->assertSame($this->readRepo, $v2->getRepository());\n        $this->assertSame('BODY2', $v2->getBody());\n    }\n\n    /**\n     * @depends testOverrideSuperPath\n     * @depends testOverrideSubPath\n     */\n    public function testOverrideSuperAndSubPathShortFirst()\n    {\n        $this->writeRepo->add('/webmozart', $this->prepareFixtures($this->createDirectory('/dir2', array(\n            $this->createDirectory('/puli', array(\n                $this->createFile('/file', 'BODY0'),\n            )),\n        ))));\n\n        $this->writeRepo->add('/webmozart/puli', $this->prepareFixtures($this->createDirectory('/dir1', array(\n            $this->createFile('/file', 'BODY1'),\n        ))));\n\n        $this->writeRepo->add('/webmozart/puli/file', $this->prepareFixtures($this->createFile('/file', 'BODY2')));\n\n        $this->writeRepo->add('/webmozart', $this->prepareFixtures($this->createDirectory('/dir3', array(\n            $this->createDirectory('/puli', array(\n                $this->createFile('/file', 'BODY3'),\n            )),\n        ))));\n\n        $versions = $this->readRepo->getVersions('/webmozart/puli/file');\n\n        $this->assertSame(array(0, 1, 2, 3), $versions->getVersions());\n\n        $v0 = $versions->get(0);\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $v0);\n        $this->assertSame('/webmozart/puli/file', $v0->getPath());\n        $this->assertSame($this->readRepo, $v0->getRepository());\n        $this->assertSame('BODY0', $v0->getBody());\n\n        $v1 = $versions->get(1);\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $v1);\n        $this->assertSame('/webmozart/puli/file', $v1->getPath());\n        $this->assertSame($this->readRepo, $v1->getRepository());\n        $this->assertSame('BODY1', $v1->getBody());\n\n        $v2 = $versions->get(2);\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $v2);\n        $this->assertSame('/webmozart/puli/file', $v2->getPath());\n        $this->assertSame($this->readRepo, $v2->getRepository());\n        $this->assertSame('BODY2', $v2->getBody());\n\n        $v3 = $versions->get(3);\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $v3);\n        $this->assertSame('/webmozart/puli/file', $v3->getPath());\n        $this->assertSame($this->readRepo, $v3->getRepository());\n        $this->assertSame('BODY3', $v3->getBody());\n    }\n\n    /**\n     * @depends testOverrideSuperPath\n     * @depends testOverrideSubPath\n     */\n    public function testOverrideSuperAndSubPathMediumFirst()\n    {\n        $this->writeRepo->add('/webmozart/puli', $this->prepareFixtures($this->createDirectory('/dir1', array(\n            $this->createFile('/file', 'BODY0'),\n        ))));\n\n        $this->writeRepo->add('/webmozart', $this->prepareFixtures($this->createDirectory('/dir2', array(\n            $this->createDirectory('/puli', array(\n                $this->createFile('/file', 'BODY1'),\n            )),\n        ))));\n\n        $this->writeRepo->add('/webmozart/puli/file', $this->prepareFixtures($this->createFile('/file', 'BODY2')));\n\n        $this->writeRepo->add('/webmozart', $this->prepareFixtures($this->createDirectory('/dir3', array(\n            $this->createDirectory('/puli', array(\n                $this->createFile('/file', 'BODY3'),\n            )),\n        ))));\n\n        $versions = $this->readRepo->getVersions('/webmozart/puli/file');\n\n        $this->assertSame(array(0, 1, 2, 3), $versions->getVersions());\n\n        $v0 = $versions->get(0);\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $v0);\n        $this->assertSame('/webmozart/puli/file', $v0->getPath());\n        $this->assertSame($this->readRepo, $v0->getRepository());\n        $this->assertSame('BODY0', $v0->getBody());\n\n        $v1 = $versions->get(1);\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $v1);\n        $this->assertSame('/webmozart/puli/file', $v1->getPath());\n        $this->assertSame($this->readRepo, $v1->getRepository());\n        $this->assertSame('BODY1', $v1->getBody());\n\n        $v2 = $versions->get(2);\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $v2);\n        $this->assertSame('/webmozart/puli/file', $v2->getPath());\n        $this->assertSame($this->readRepo, $v2->getRepository());\n        $this->assertSame('BODY2', $v2->getBody());\n\n        $v3 = $versions->get(3);\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $v3);\n        $this->assertSame('/webmozart/puli/file', $v3->getPath());\n        $this->assertSame($this->readRepo, $v3->getRepository());\n        $this->assertSame('BODY3', $v3->getBody());\n    }\n\n    /**\n     * @depends testOverrideSuperPath\n     * @depends testOverrideSubPath\n     */\n    public function testOverrideSuperAndSubPathLongFirst()\n    {\n        $this->writeRepo->add('/webmozart/puli/file', $this->prepareFixtures($this->createFile('/file', 'BODY0')));\n\n        $this->writeRepo->add('/webmozart', $this->prepareFixtures($this->createDirectory('/dir2', array(\n            $this->createDirectory('/puli', array(\n                $this->createFile('/file', 'BODY1'),\n            )),\n        ))));\n\n        $this->writeRepo->add('/webmozart/puli', $this->prepareFixtures($this->createDirectory('/dir1', array(\n            $this->createFile('/file', 'BODY2'),\n        ))));\n\n        $this->writeRepo->add('/webmozart', $this->prepareFixtures($this->createDirectory('/dir3', array(\n            $this->createDirectory('/puli', array(\n                $this->createFile('/file', 'BODY3'),\n            )),\n        ))));\n\n        $versions = $this->readRepo->getVersions('/webmozart/puli/file');\n\n        $this->assertSame(array(0, 1, 2, 3), $versions->getVersions());\n\n        $v0 = $versions->get(0);\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $v0);\n        $this->assertSame('/webmozart/puli/file', $v0->getPath());\n        $this->assertSame($this->readRepo, $v0->getRepository());\n        $this->assertSame('BODY0', $v0->getBody());\n\n        $v1 = $versions->get(1);\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $v1);\n        $this->assertSame('/webmozart/puli/file', $v1->getPath());\n        $this->assertSame($this->readRepo, $v1->getRepository());\n        $this->assertSame('BODY1', $v1->getBody());\n\n        $v2 = $versions->get(2);\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $v2);\n        $this->assertSame('/webmozart/puli/file', $v2->getPath());\n        $this->assertSame($this->readRepo, $v2->getRepository());\n        $this->assertSame('BODY2', $v2->getBody());\n\n        $v3 = $versions->get(3);\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $v3);\n        $this->assertSame('/webmozart/puli/file', $v3->getPath());\n        $this->assertSame($this->readRepo, $v3->getRepository());\n        $this->assertSame('BODY3', $v3->getBody());\n    }\n\n    /**\n     * @ depends testOverrideSuperPath\n     * @ depends testOverrideSubPath\n     */\n    public function testOverrideFourLevels()\n    {\n        $this->writeRepo->add('/webmozart/puli', $this->prepareFixtures($this->createDirectory('/dir1', array(\n            $this->createDirectory('/sub', array(\n                $this->createFile('/file', 'BODY0'),\n            )),\n        ))));\n\n        $this->writeRepo->add('/webmozart/puli/sub', $this->prepareFixtures($this->createDirectory('/dir2', array(\n            $this->createFile('/file', 'BODY1'),\n        ))));\n\n        $this->writeRepo->add('/webmozart/puli/sub/file', $this->prepareFixtures($this->createFile('/file', 'BODY2')));\n\n        $this->writeRepo->add('/webmozart', $this->prepareFixtures($this->createDirectory('/dir3', array(\n            $this->createDirectory('/puli', array(\n                $this->createDirectory('/sub', array(\n                    $this->createFile('/file', 'BODY3'),\n                )),\n            )),\n        ))));\n\n        $versions = $this->readRepo->getVersions('/webmozart/puli/sub/file');\n\n        $this->assertSame(array(0, 1, 2, 3), $versions->getVersions());\n\n        $v0 = $versions->get(0);\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $v0);\n        $this->assertSame('/webmozart/puli/sub/file', $v0->getPath());\n        $this->assertSame($this->readRepo, $v0->getRepository());\n        $this->assertSame('BODY0', $v0->getBody());\n\n        $v1 = $versions->get(1);\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $v1);\n        $this->assertSame('/webmozart/puli/sub/file', $v1->getPath());\n        $this->assertSame($this->readRepo, $v1->getRepository());\n        $this->assertSame('BODY1', $v1->getBody());\n\n        $v2 = $versions->get(2);\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $v2);\n        $this->assertSame('/webmozart/puli/sub/file', $v2->getPath());\n        $this->assertSame($this->readRepo, $v2->getRepository());\n        $this->assertSame('BODY2', $v2->getBody());\n\n        $v3 = $versions->get(3);\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $v3);\n        $this->assertSame('/webmozart/puli/sub/file', $v3->getPath());\n        $this->assertSame($this->readRepo, $v3->getRepository());\n        $this->assertSame('BODY3', $v3->getBody());\n    }\n\n    public function testRemoveFile()\n    {\n        $this->writeRepo->add('/webmozart/puli/file1', $this->prepareFixtures($this->createFile()));\n        $this->writeRepo->add('/webmozart/puli/file2', $this->prepareFixtures($this->createFile()));\n\n        $this->assertTrue($this->readRepo->contains('/webmozart'));\n        $this->assertTrue($this->readRepo->contains('/webmozart/puli'));\n        $this->assertTrue($this->readRepo->contains('/webmozart/puli/file1'));\n        $this->assertTrue($this->readRepo->contains('/webmozart/puli/file2'));\n\n        $this->assertSame(1, $this->writeRepo->remove('/webmozart/puli/file1'));\n\n        $this->readRepo = $this->createReadRepository($this->writeRepo);\n\n        $this->assertTrue($this->readRepo->contains('/webmozart'));\n        $this->assertTrue($this->readRepo->contains('/webmozart/puli'));\n        $this->assertFalse($this->readRepo->contains('/webmozart/puli/file1'));\n        $this->assertTrue($this->readRepo->contains('/webmozart/puli/file2'));\n    }\n\n    public function testRemoveMany()\n    {\n        $this->writeRepo->add('/webmozart/puli/file1', $this->prepareFixtures($this->createFile()));\n        $this->writeRepo->add('/webmozart/puli/file2', $this->prepareFixtures($this->createFile()));\n\n        $this->assertTrue($this->readRepo->contains('/webmozart/puli'));\n        $this->assertTrue($this->readRepo->contains('/webmozart/puli/file1'));\n        $this->assertTrue($this->readRepo->contains('/webmozart/puli/file2'));\n\n        $this->assertSame(2, $this->writeRepo->remove('/webmozart/puli/file*'));\n\n        $this->readRepo = $this->createReadRepository($this->writeRepo);\n\n        $this->assertTrue($this->readRepo->contains('/webmozart/puli'));\n        $this->assertFalse($this->readRepo->contains('/webmozart/puli/file1'));\n        $this->assertFalse($this->readRepo->contains('/webmozart/puli/file2'));\n    }\n\n    public function provideDirectoryGlob()\n    {\n        return array(\n            array('/webmozart/puli'),\n            array('/webmozart/pu*'),\n        );\n    }\n\n    /**\n     * @dataProvider provideDirectoryGlob\n     */\n    public function testRemoveDirectory($glob)\n    {\n        $this->writeRepo->add('/webmozart/puli/file1', $this->prepareFixtures($this->createFile()));\n        $this->writeRepo->add('/webmozart/puli/file2', $this->prepareFixtures($this->createFile()));\n\n        $this->assertTrue($this->readRepo->contains('/webmozart'));\n        $this->assertTrue($this->readRepo->contains('/webmozart/puli'));\n        $this->assertTrue($this->readRepo->contains('/webmozart/puli/file1'));\n        $this->assertTrue($this->readRepo->contains('/webmozart/puli/file2'));\n\n        $this->assertSame(3, $this->writeRepo->remove($glob));\n\n        $this->readRepo = $this->createReadRepository($this->writeRepo);\n\n        $this->assertTrue($this->readRepo->contains('/webmozart'));\n        $this->assertFalse($this->readRepo->contains('/webmozart/puli'));\n        $this->assertFalse($this->readRepo->contains('/webmozart/puli/file1'));\n        $this->assertFalse($this->readRepo->contains('/webmozart/puli/file2'));\n    }\n\n    public function testRemoveDot()\n    {\n        $this->writeRepo->add('/webmozart/puli/file1', $this->prepareFixtures($this->createFile()));\n        $this->writeRepo->add('/webmozart/puli/file2', $this->prepareFixtures($this->createFile()));\n\n        $this->assertTrue($this->readRepo->contains('/webmozart'));\n        $this->assertTrue($this->readRepo->contains('/webmozart/puli'));\n        $this->assertTrue($this->readRepo->contains('/webmozart/puli/file1'));\n        $this->assertTrue($this->readRepo->contains('/webmozart/puli/file2'));\n\n        $this->writeRepo->remove('/webmozart/puli/.');\n\n        $this->readRepo = $this->createReadRepository($this->writeRepo);\n\n        $this->assertTrue($this->readRepo->contains('/webmozart'));\n        $this->assertFalse($this->readRepo->contains('/webmozart/puli'));\n        $this->assertFalse($this->readRepo->contains('/webmozart/puli/file1'));\n        $this->assertFalse($this->readRepo->contains('/webmozart/puli/file2'));\n    }\n\n    public function testRemoveDotDot()\n    {\n        $this->writeRepo->add('/webmozart/puli/file1', $this->prepareFixtures($this->createFile()));\n        $this->writeRepo->add('/webmozart/puli/file2', $this->prepareFixtures($this->createFile()));\n\n        $this->assertTrue($this->readRepo->contains('/'));\n        $this->assertTrue($this->readRepo->contains('/webmozart'));\n        $this->assertTrue($this->readRepo->contains('/webmozart/puli'));\n        $this->assertTrue($this->readRepo->contains('/webmozart/puli/file1'));\n        $this->assertTrue($this->readRepo->contains('/webmozart/puli/file2'));\n\n        $this->writeRepo->remove('/webmozart/puli/..');\n\n        $this->readRepo = $this->createReadRepository($this->writeRepo);\n\n        $this->assertTrue($this->readRepo->contains('/'));\n        $this->assertFalse($this->readRepo->contains('/webmozart'));\n        $this->assertFalse($this->readRepo->contains('/webmozart/puli'));\n        $this->assertFalse($this->readRepo->contains('/webmozart/puli/file1'));\n        $this->assertFalse($this->readRepo->contains('/webmozart/puli/file2'));\n    }\n\n    public function testRemoveDiscardsTrailingSlash()\n    {\n        $this->writeRepo->add('/webmozart/puli/file1', $this->prepareFixtures($this->createFile()));\n        $this->writeRepo->add('/webmozart/puli/file2', $this->prepareFixtures($this->createFile()));\n\n        $this->assertTrue($this->readRepo->contains('/webmozart/puli'));\n        $this->assertTrue($this->readRepo->contains('/webmozart/puli/file1'));\n        $this->assertTrue($this->readRepo->contains('/webmozart/puli/file2'));\n\n        $this->writeRepo->remove('/webmozart/puli/');\n\n        $this->readRepo = $this->createReadRepository($this->writeRepo);\n\n        $this->assertFalse($this->readRepo->contains('/webmozart/puli'));\n        $this->assertFalse($this->readRepo->contains('/webmozart/puli/file1'));\n        $this->assertFalse($this->readRepo->contains('/webmozart/puli/file2'));\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testCannotRemoveRoot()\n    {\n        $this->writeRepo->remove('/');\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testRemoveInterpretsConsecutiveSlashesAsRoot()\n    {\n        $this->writeRepo->remove('///');\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testRemoveExpectsAbsolutePath()\n    {\n        $this->writeRepo->remove('webmozart');\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testRemoveExpectsNonEmptyPath()\n    {\n        $this->writeRepo->remove('');\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testRemoveExpectsStringPath()\n    {\n        $this->writeRepo->remove(new \\stdClass());\n    }\n\n    public function testClear()\n    {\n        $this->writeRepo->add('/webmozart/puli/file1', $this->prepareFixtures($this->createFile()));\n        $this->writeRepo->add('/webmozart/puli/file2', $this->prepareFixtures($this->createFile()));\n\n        $this->assertTrue($this->readRepo->contains('/'));\n        $this->assertTrue($this->readRepo->contains('/webmozart'));\n        $this->assertTrue($this->readRepo->contains('/webmozart/puli'));\n        $this->assertTrue($this->readRepo->contains('/webmozart/puli/file1'));\n        $this->assertTrue($this->readRepo->contains('/webmozart/puli/file2'));\n\n        $this->assertSame(4, $this->writeRepo->clear());\n\n        $this->readRepo = $this->createReadRepository($this->writeRepo);\n\n        $this->assertTrue($this->readRepo->contains('/'));\n        $this->assertFalse($this->readRepo->contains('/webmozart'));\n        $this->assertFalse($this->readRepo->contains('/webmozart/puli'));\n        $this->assertFalse($this->readRepo->contains('/webmozart/puli/file1'));\n        $this->assertFalse($this->readRepo->contains('/webmozart/puli/file2'));\n    }\n\n    public function testFileLink()\n    {\n        $this->writeRepo->add('/webmozart/file', new FileResource(__DIR__.'/Fixtures/dir1/file1'));\n        $this->writeRepo->add('/webmozart/link', new LinkResource('/webmozart/file'));\n\n        $link = $this->readRepo->get('/webmozart/link');\n\n        $this->assertInstanceOf('Puli\\Repository\\Resource\\LinkResource', $link);\n        $this->assertSame('/webmozart/link', $link->getPath());\n        $this->assertSame('/webmozart/file', $link->getTargetPath());\n        $this->assertSame($this->readRepo, $link->getRepository());\n\n        $target = $link->getTarget();\n\n        $this->assertInstanceOf('Puli\\Repository\\Resource\\FileResource', $target);\n        $this->assertSame('/webmozart/file', $target->getPath());\n        $this->assertSame($this->readRepo, $target->getRepository());\n\n        $target = $this->readRepo->get($link->getTargetPath());\n\n        $this->assertInstanceOf('Puli\\Repository\\Resource\\FileResource', $target);\n        $this->assertSame('/webmozart/file', $target->getPath());\n        $this->assertSame($this->readRepo, $target->getRepository());\n    }\n\n    public function testDirectoryLink()\n    {\n        $this->writeRepo->add('/webmozart/link/foo', new DirectoryResource(__DIR__.'/Fixtures/dir1'));\n        $this->writeRepo->add('/webmozart/link/bar', new LinkResource('/webmozart/link/foo'));\n\n        $link = $this->readRepo->get('/webmozart/link/bar');\n\n        $this->assertInstanceOf('Puli\\Repository\\Resource\\LinkResource', $link);\n        $this->assertSame('/webmozart/link/bar', $link->getPath());\n        $this->assertSame('/webmozart/link/foo', $link->getTargetPath());\n        $this->assertSame($this->readRepo, $link->getRepository());\n\n        $target = $this->readRepo->get($link->getTargetPath());\n\n        $this->assertTrue($target instanceof TestDirectory || $target instanceof DirectoryResource);\n        $this->assertSame('/webmozart/link/foo', $target->getPath());\n        $this->assertSame($this->readRepo, $target->getRepository());\n        $this->assertCount(2, $target->listChildren());\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\NoVersionFoundException\n     */\n    public function testGetVersionsFailsForDeletedResources()\n    {\n        $this->writeRepo->add('/webmozart/file', $this->prepareFixtures($this->createFile()));\n        $this->writeRepo->remove('/webmozart/file');\n\n        // We cannot guarantee that all repository implementations maintain\n        // information about deleted resources\n        $this->readRepo->getVersions('/webmozart/file');\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\NoVersionFoundException\n     */\n    public function testGetVersionsFailsForChildrenOfDeletedResources()\n    {\n        $this->writeRepo->add('/webmozart', $this->prepareFixtures($this->createDirectory()));\n        $this->writeRepo->add('/webmozart/file', $this->prepareFixtures($this->createFile()));\n        $this->writeRepo->remove('/webmozart');\n\n        $this->readRepo->getVersions('/webmozart/file');\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\NoVersionFoundException\n     */\n    public function testGetVersionsFailsAfterClearing()\n    {\n        $this->writeRepo->add('/webmozart/file', $this->prepareFixtures($this->createFile()));\n        $this->writeRepo->clear();\n\n        $this->readRepo->getVersions('/webmozart/file');\n    }\n\n    public function testGetVersionsSucceedsForRootAfterClearing()\n    {\n        $this->writeRepo->clear();\n\n        $this->assertCount(1, $this->readRepo->getVersions('/'));\n    }\n\n    public function testGetVersionsDoesNotIncludeDeletedResources()\n    {\n        $this->writeRepo->add('/webmozart/file', $this->prepareFixtures($this->createFile()));\n        $this->writeRepo->remove('/webmozart/file');\n        $this->writeRepo->add('/webmozart/file', $this->prepareFixtures($this->createFile(null, 'NEW BODY')));\n\n        $versions = $this->readRepo->getVersions('/webmozart/file');\n\n        $this->assertSame(array(0), $versions->getVersions());\n\n        $resource = $versions->getFirst();\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $resource);\n        $this->assertSame('/webmozart/file', $resource->getPath());\n        $this->assertSame($this->readRepo, $resource->getRepository());\n        $this->assertSame('NEW BODY', $resource->getBody());\n    }\n}\n"
  },
  {
    "path": "tests/AbstractFilesystemRepositorySymlinkTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests;\n\nuse Puli\\Repository\\Resource\\DirectoryResource;\nuse Symfony\\Component\\Filesystem\\Filesystem;\nuse Webmozart\\Glob\\Test\\TestUtil;\n\n/**\n * @author Titouan Galopin <galopintitouan@gmail.com>\n */\nabstract class AbstractFilesystemRepositorySymlinkTest extends AbstractFilesystemRepositoryTest\n{\n    protected $tempBaseDir;\n\n    protected $tempDir;\n\n    /**\n     * Copy fixtures to temporary directory to prevent messing up the real\n     * fixtures when symlinks do not work.\n     */\n    protected $tempFixtures;\n\n    protected function setUp()\n    {\n        $this->markAsSkippedIfSymlinkIsMissing();\n\n        $this->tempBaseDir = TestUtil::makeTempDir('puli-repository', __CLASS__);\n\n        // Create both directories in the same directory, so that relative links\n        // work from one to the other\n        $this->tempDir = $this->tempBaseDir.'/workspace';\n        $this->tempFixtures = $this->tempBaseDir.'/fixtures';\n\n        mkdir($this->tempDir);\n        mkdir($this->tempFixtures);\n\n        $filesystem = new Filesystem();\n        $filesystem->mirror(__DIR__.'/Fixtures', $this->tempFixtures);\n\n        parent::setUp();\n    }\n\n    protected function tearDown()\n    {\n        parent::tearDown();\n\n        $filesystem = new Filesystem();\n        $filesystem->remove($this->tempBaseDir);\n    }\n\n    public function testClearDirectoryLinksDoesNotRemoveChildrenFiles()\n    {\n        $this->writeRepo->add('/webmozart', new DirectoryResource($this->tempFixtures.'/dir2'));\n\n        $this->assertTrue($this->writeRepo->contains('/webmozart/file2'));\n        $this->assertTrue($this->writeRepo->contains('/webmozart/file3'));\n\n        $this->writeRepo->clear();\n\n        // We should not be able to access the resources\n        $this->assertFalse($this->writeRepo->contains('/webmozart/file2'));\n        $this->assertFalse($this->writeRepo->contains('/webmozart/file3'));\n\n        // But the files should still be here\n        $this->assertFileExists($this->tempFixtures.'/dir2/file2');\n        $this->assertFileExists($this->tempFixtures.'/dir2/file3');\n    }\n}\n"
  },
  {
    "path": "tests/AbstractFilesystemRepositoryTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nabstract class AbstractFilesystemRepositoryTest extends AbstractEditableRepositoryTest\n{\n    protected function assertPathsAreEqual($expected, $actual)\n    {\n        $normalize = function ($path) {\n            return str_replace(DIRECTORY_SEPARATOR, '/', $path);\n        };\n\n        $this->assertEquals($normalize($expected), $normalize($actual));\n    }\n}\n"
  },
  {
    "path": "tests/AbstractJsonRepositoryTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests;\n\nuse Psr\\Log\\LogLevel;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\nuse Puli\\Repository\\Resource\\DirectoryResource;\nuse Puli\\Repository\\Resource\\FileResource;\nuse Puli\\Repository\\Tests\\Resource\\TestDirectory;\nuse Symfony\\Component\\Filesystem\\Filesystem;\nuse Webmozart\\Glob\\Test\\TestUtil;\nuse Webmozart\\PathUtil\\Path;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n * @author Titouan Galopin <galopintitouan@gmail.com>\n */\nabstract class AbstractJsonRepositoryTest extends AbstractEditableRepositoryTest\n{\n    /**\n     * @var string\n     */\n    protected $path;\n\n    /**\n     * @var string\n     */\n    protected $tempDir;\n\n    /**\n     * Contains a copy of the static fixtures.\n     *\n     * @var string\n     */\n    protected $fixtureDir;\n\n    /**\n     * Contains dynamically created fixtures.\n     *\n     * @var string\n     */\n    protected $tempFixtureDir;\n\n    /**\n     * Counter to avoid collisions during tests on files.\n     *\n     * @var int\n     */\n    private $nextFileId;\n\n    /**\n     * Counter to avoid collisions during tests on directories.\n     *\n     * @var int\n     */\n    private $nextDirectoryId;\n\n    protected function setUp()\n    {\n        $this->tempDir = TestUtil::makeTempDir('puli-repository', __CLASS__);\n        $this->fixtureDir = $this->tempDir.'/fixtures';\n        $this->tempFixtureDir = $this->tempDir.'/temp-fixtures';\n        $this->path = $this->tempDir.'/puli.json';\n        $this->nextFileId = 0;\n        $this->nextDirectoryId = 0;\n\n        $filesystem = new Filesystem();\n        $filesystem->mkdir($this->tempFixtureDir);\n        $filesystem->mirror(__DIR__.'/Fixtures', $this->fixtureDir);\n\n        parent::setUp();\n    }\n\n    protected function tearDown()\n    {\n        parent::tearDown();\n\n        $filesystem = new Filesystem();\n        $filesystem->remove($this->tempDir);\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedLanguageException\n     * @expectedExceptionMessage foobar\n     */\n    public function testContainsFailsIfLanguageNotGlob()\n    {\n        $this->readRepo->contains('/*', 'foobar');\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedLanguageException\n     * @expectedExceptionMessage foobar\n     */\n    public function testFindFailsIfLanguageNotGlob()\n    {\n        $this->readRepo->find('/*', 'foobar');\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedLanguageException\n     * @expectedExceptionMessage foobar\n     */\n    public function testRemoveFailsIfLanguageNotGlob()\n    {\n        $this->writeRepo->remove('/*', 'foobar');\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\ResourceNotFoundException\n     */\n    public function testGetLogsWarningIfReferenceNotFound()\n    {\n        $this->writeRepo->add('/file', new FileResource($this->fixtureDir.'/dir1/file1'));\n\n        unlink($this->fixtureDir.'/dir1/file1');\n\n        $logger = $this->getMock('Psr\\Log\\LoggerInterface');\n        $logger->expects($this->once())\n            ->method('log')\n            ->with(LogLevel::WARNING, $this->stringContains('\"fixtures/dir1/file1\"'));\n\n        $this->readRepo->setLogger($logger);\n\n        $this->readRepo->get('/file');\n    }\n\n    public function testFindLogsWarningIfReferenceNotFound()\n    {\n        $this->writeRepo->add('/file', new FileResource($this->fixtureDir.'/dir1/file1'));\n\n        unlink($this->fixtureDir.'/dir1/file1');\n\n        $logger = $this->getMock('Psr\\Log\\LoggerInterface');\n        $logger->expects($this->once())\n            ->method('log')\n            ->with(LogLevel::WARNING, $this->stringContains('\"fixtures/dir1/file1\"'));\n\n        $this->readRepo->setLogger($logger);\n\n        $this->assertCount(0, $this->readRepo->find('/fi*'));\n    }\n\n    public function testContainsLogsWarningIfReferenceNotFound()\n    {\n        $this->writeRepo->add('/file', new FileResource($this->fixtureDir.'/dir1/file1'));\n\n        unlink($this->fixtureDir.'/dir1/file1');\n\n        $logger = $this->getMock('Psr\\Log\\LoggerInterface');\n        $logger->expects($this->once())\n            ->method('log')\n            ->with(LogLevel::WARNING, $this->stringContains('\"fixtures/dir1/file1\"'));\n\n        $this->readRepo->setLogger($logger);\n\n        $this->assertFalse($this->readRepo->contains('/fi*'));\n    }\n\n    public function testHasChildrenLogsWarningIfReferenceNotFound()\n    {\n        $this->writeRepo->add('/webmozart/file', new FileResource($this->fixtureDir.'/dir1/file1'));\n\n        unlink($this->fixtureDir.'/dir1/file1');\n\n        $logger = $this->getMock('Psr\\Log\\LoggerInterface');\n        $logger->expects($this->once())\n            ->method('log')\n            ->with(LogLevel::WARNING, $this->stringContains('\"fixtures/dir1/file1\"'));\n\n        $this->readRepo->setLogger($logger);\n\n        $this->assertFalse($this->readRepo->hasChildren('/webmozart'));\n    }\n\n    public function testListChildrenLogsWarningIfReferenceNotFound()\n    {\n        $this->writeRepo->add('/webmozart/file', new FileResource($this->fixtureDir.'/dir1/file1'));\n\n        unlink($this->fixtureDir.'/dir1/file1');\n\n        $logger = $this->getMock('Psr\\Log\\LoggerInterface');\n        $logger->expects($this->once())\n            ->method('log')\n            ->with(LogLevel::WARNING, $this->stringContains('\"fixtures/dir1/file1\"'));\n\n        $this->readRepo->setLogger($logger);\n\n        $this->assertCount(0, $this->readRepo->listChildren('/webmozart'));\n    }\n\n    protected function prepareFixtures(PuliResource $root)\n    {\n        return $this->copyToFilesystem($root);\n    }\n\n    /**\n     * @param PuliResource $resource\n     * @param string       $parentPath\n     *\n     * @return DirectoryResource|FileResource\n     */\n    private function copyToFilesystem($resource, $parentPath = '')\n    {\n        $filesystem = new Filesystem();\n\n        if ($resource instanceof TestDirectory) {\n            $directoryPath = null === $resource->getPath()\n                ? $parentPath.'/dir'.($this->nextDirectoryId++)\n                : $parentPath.rtrim($resource->getPath(), '/');\n\n            $filesystem->mkdir($this->tempFixtureDir.$directoryPath);\n\n            foreach ($resource->listChildren() as $child) {\n                $this->copyToFilesystem($child, $directoryPath);\n            }\n\n            return new DirectoryResource($this->tempFixtureDir.$directoryPath, $resource->getPath());\n        }\n\n        $filePath = null === $resource->getPath()\n            ? $parentPath.'/file'.($this->nextFileId++)\n            : $parentPath.rtrim($resource->getPath(), '/');\n\n        $filesystem->mkdir(Path::getDirectory($this->tempFixtureDir.$filePath));\n\n        file_put_contents($this->tempFixtureDir.$filePath, $resource->getBody());\n\n        return new FileResource($this->tempFixtureDir.$filePath, $resource->getPath());\n    }\n}\n"
  },
  {
    "path": "tests/AbstractRepositoryTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests;\n\nuse PHPUnit_Framework_TestCase;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\nuse Puli\\Repository\\Api\\ResourceRepository;\nuse Puli\\Repository\\Tests\\Resource\\TestDirectory;\nuse Puli\\Repository\\Tests\\Resource\\TestFile;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nabstract class AbstractRepositoryTest extends PHPUnit_Framework_TestCase\n{\n    /**\n     * @param PuliResource $root\n     *\n     * @return ResourceRepository\n     */\n    abstract protected function createPrefilledRepository(PuliResource $root);\n\n    /**\n     * @param string $path\n     * @param string $body\n     *\n     * @return TestFile\n     */\n    protected function createFile($path = null, $body = TestFile::BODY)\n    {\n        return new TestFile($path, $body);\n    }\n\n    /**\n     * @param string $path\n     * @param array  $children\n     *\n     * @return TestDirectory\n     */\n    protected function createDirectory($path = null, array $children = array())\n    {\n        return new TestDirectory($path, $children);\n    }\n\n    /**\n     * Build the real backend structure.\n     *\n     * @param PuliResource $root\n     *\n     * @return PuliResource\n     */\n    protected function prepareFixtures(PuliResource $root)\n    {\n        return $root;\n    }\n\n    protected function pass()\n    {\n        $this->assertTrue(true);\n    }\n\n    public function testContainsPath()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/')));\n\n        $this->assertTrue($repo->contains('/'));\n        $this->assertTrue($repo->contains('/.'));\n        $this->assertTrue($repo->contains('/..'));\n        $this->assertFalse($repo->contains('/webmozart'));\n        $this->assertFalse($repo->contains('/webmozart/.'));\n        $this->assertTrue($repo->contains('/webmozart/..'));\n        $this->assertFalse($repo->contains('/./webmozart'));\n        $this->assertFalse($repo->contains('/../webmozart'));\n        $this->assertFalse($repo->contains('/webmozart/../webmozart'));\n        $this->assertFalse($repo->contains('/webmozart/puli'));\n        $this->assertFalse($repo->contains('/webmozart/puli/.'));\n        $this->assertFalse($repo->contains('/webmozart/puli/..'));\n        $this->assertFalse($repo->contains('/webmozart/./puli'));\n        $this->assertFalse($repo->contains('/webmozart/././puli'));\n        $this->assertFalse($repo->contains('/webmozart/../webmozart/puli'));\n        $this->assertFalse($repo->contains('/webmozart/../../webmozart/puli'));\n        $this->assertFalse($repo->contains('/webmozart/../puli'));\n        $this->assertFalse($repo->contains('/webmozart/./../puli'));\n        $this->assertFalse($repo->contains('/webmozart/.././puli'));\n        $this->assertFalse($repo->contains('/webmozart/puli/file1'));\n        $this->assertFalse($repo->contains('/webmozart/puli/file1/.'));\n        $this->assertFalse($repo->contains('/webmozart/puli/file1/..'));\n        $this->assertFalse($repo->contains('/webmozart/puli/file2'));\n        $this->assertFalse($repo->contains('/webmozart/puli/file2/.'));\n        $this->assertFalse($repo->contains('/webmozart/puli/file2/..'));\n\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart', array(\n                $this->createDirectory('/puli', array(\n                    $this->createFile('/file1'),\n                    $this->createFile('/file2'),\n                )),\n            )),\n        ))));\n\n        $this->assertTrue($repo->contains('/'));\n        $this->assertTrue($repo->contains('/.'));\n        $this->assertTrue($repo->contains('/..'));\n        $this->assertTrue($repo->contains('/webmozart'));\n        $this->assertTrue($repo->contains('/webmozart/.'));\n        $this->assertTrue($repo->contains('/webmozart/..'));\n        $this->assertTrue($repo->contains('/./webmozart'));\n        $this->assertTrue($repo->contains('/../webmozart'));\n        $this->assertTrue($repo->contains('/webmozart/puli'));\n        $this->assertTrue($repo->contains('/webmozart/puli/.'));\n        $this->assertTrue($repo->contains('/webmozart/puli/..'));\n        $this->assertTrue($repo->contains('/webmozart/./puli'));\n        $this->assertTrue($repo->contains('/webmozart/././puli'));\n        $this->assertTrue($repo->contains('/webmozart/../webmozart/puli'));\n        $this->assertTrue($repo->contains('/webmozart/../../webmozart/puli'));\n        $this->assertFalse($repo->contains('/webmozart/../puli'));\n        $this->assertFalse($repo->contains('/webmozart/./../puli'));\n        $this->assertFalse($repo->contains('/webmozart/.././puli'));\n        $this->assertTrue($repo->contains('/webmozart/puli/file1'));\n        $this->assertTrue($repo->contains('/webmozart/puli/file1/.'));\n        $this->assertTrue($repo->contains('/webmozart/puli/file1/..'));\n        $this->assertTrue($repo->contains('/webmozart/puli/file2'));\n        $this->assertTrue($repo->contains('/webmozart/puli/file2/.'));\n        $this->assertTrue($repo->contains('/webmozart/puli/file2/..'));\n    }\n\n    public function testContainsPattern()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/')));\n\n        $this->assertFalse($repo->contains('/webmozart/**/*'));\n        $this->assertFalse($repo->contains('/webmozart/file*'));\n        $this->assertFalse($repo->contains('/webmozart/puli/file*'));\n        $this->assertFalse($repo->contains('/webmozart/**/file*'));\n\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart', array(\n                $this->createDirectory('/puli', array(\n                    $this->createFile('/file1'),\n                    $this->createFile('/file2'),\n                )),\n            )),\n        ))));\n\n        $this->assertTrue($repo->contains('/**/*'));\n        $this->assertTrue($repo->contains('/webmozart/**/*'));\n        $this->assertFalse($repo->contains('/webmozart/file*'));\n        $this->assertTrue($repo->contains('/webmozart/puli/file*'));\n        $this->assertTrue($repo->contains('/**/file*'));\n        $this->assertTrue($repo->contains('/webmozart/**/file*'));\n    }\n\n    public function testContainsDiscardsTrailingSlash()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart'),\n        ))));\n\n        $this->assertTrue($repo->contains('/webmozart/'));\n    }\n\n    public function testContainsInterpretsConsecutiveSlashesAsRoot()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/')));\n\n        $this->assertTrue($repo->contains('///'));\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testContainsExpectsAbsolutePath()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart'),\n        ))));\n\n        $repo->contains('webmozart');\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testContainsExpectsNonEmptyPath()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/')));\n\n        $repo->contains('');\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testContainsExpectsStringPath()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/')));\n\n        $repo->contains(new \\stdClass());\n    }\n\n    public function testGetResource()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart', array(\n                $this->createDirectory('/puli'),\n            )),\n        ))));\n\n        $resource = $repo->get('/webmozart');\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\PuliResource', $resource);\n        $this->assertSame('/webmozart', $resource->getPath());\n        $this->assertSame($repo, $resource->getRepository());\n        $this->assertTrue($resource->hasChildren());\n    }\n\n    public function testGetBodyResource()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart', array(\n                $this->createDirectory('/puli', array(\n                    $this->createFile('/file'),\n                )),\n            )),\n        ))));\n\n        $resource = $repo->get('/webmozart/puli/file');\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\Resource\\BodyResource', $resource);\n        $this->assertSame('/webmozart/puli/file', $resource->getPath());\n        $this->assertSame($repo, $resource->getRepository());\n        $this->assertFalse($resource->hasChildren());\n    }\n\n    public function testGetDiscardsTrailingSlash()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart'),\n        ))));\n\n        $this->assertEquals($repo->get('/webmozart'), $repo->get('/webmozart/'));\n    }\n\n    public function testGetInterpretsConsecutiveSlashesAsRoot()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/')));\n\n        $this->assertEquals($repo->get('/'), $repo->get('///'));\n    }\n\n    public function testGetCanonicalizesFilePaths()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart', array(\n                $this->createDirectory('/puli', array(\n                    $this->createFile('/file'),\n                )),\n            )),\n        ))));\n\n        $this->assertEquals($repo->get('/webmozart/puli/file'), $repo->get('/webmozart/puli/../puli/./file'));\n    }\n\n    public function testGetCanonicalizesDirectoryPaths()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart', array(\n                $this->createDirectory('/puli', array(\n                    $this->createDirectory('/dir'),\n                )),\n            )),\n        ))));\n\n        $this->assertEquals($repo->get('/webmozart/puli/dir'), $repo->get('/webmozart/puli/../puli/dir'));\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\ResourceNotFoundException\n     */\n    public function testGetExpectsExistingResource()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/')));\n\n        $repo->get('/foo/bar');\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testGetExpectsAbsolutePath()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart'),\n        ))));\n\n        $repo->get('webmozart');\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testGetExpectsNonEmptyPath()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/')));\n\n        $repo->get('');\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testGetExpectsStringPath()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/')));\n\n        $repo->get(new \\stdClass());\n    }\n\n    public function testGetDotInDirectory()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart'),\n        ))));\n\n        $this->assertEquals($repo->get('/webmozart'), $repo->get('/webmozart/.'));\n    }\n\n    public function testGetDotInFile()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart', array(\n                $this->createDirectory('/puli', array(\n                    $this->createFile('/file1'),\n                )),\n            )),\n        ))));\n\n        // We support this case even though it leads to an error if done\n        // on a regular file system, because recognizing files would be too\n        // big a performance impact\n        // You should not rely on this however, as this may change anytime\n        $this->assertEquals($repo->get('/webmozart/puli/file1'), $repo->get('/webmozart/puli/file1/.'));\n    }\n\n    public function testGetDotInRoot()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/')));\n\n        $this->assertEquals($repo->get('/'), $repo->get('/.'));\n    }\n\n    public function testGetDotDotInDirectory()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart', array(\n                $this->createDirectory('/puli'),\n            )),\n        ))));\n\n        $this->assertEquals($repo->get('/webmozart'), $repo->get('/webmozart/puli/..'));\n    }\n\n    public function testGetDotDotInFile()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart', array(\n                $this->createDirectory('/puli', array(\n                    $this->createFile('/file'),\n                )),\n            )),\n        ))));\n\n        // We support this case even though it leads to an error if done\n        // on a regular file system, because recognizing files would be too\n        // big a performance impact\n        // You should not rely on this however, as this may change anytime\n        $this->assertEquals($repo->get('/webmozart/puli'), $repo->get('/webmozart/puli/file1/..'));\n    }\n\n    public function testGetDotDotInRoot()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/')));\n\n        $this->assertEquals($repo->get('/'), $repo->get('/..'));\n    }\n\n    public function testHasChildren()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart', array(\n                $this->createDirectory('/puli', array(\n                    $this->createFile('/.dotfile'),\n                    $this->createFile('/foo'),\n                    $this->createFile('/bar'),\n                    $this->createDirectory('/dir'),\n                )),\n            )),\n        ))));\n\n        $this->assertTrue($repo->hasChildren('/'));\n        $this->assertTrue($repo->hasChildren('/webmozart'));\n        $this->assertTrue($repo->hasChildren('/webmozart/puli'));\n        $this->assertFalse($repo->hasChildren('/webmozart/puli/.dotfile'));\n        $this->assertFalse($repo->hasChildren('/webmozart/puli/foo'));\n        $this->assertFalse($repo->hasChildren('/webmozart/puli/bar'));\n        $this->assertFalse($repo->hasChildren('/webmozart/puli/dir'));\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\ResourceNotFoundException\n     */\n    public function testHasChildrenExpectsExistingResource()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/')));\n\n        $repo->hasChildren('/foo/bar');\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testHasChildrenExpectsAbsolutePath()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart'),\n        ))));\n\n        $repo->hasChildren('webmozart');\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testHasChildrenExpectsNonEmptyPath()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/')));\n\n        $repo->hasChildren('');\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testHasChildrenExpectsStringPath()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/')));\n\n        $repo->hasChildren(new \\stdClass());\n    }\n\n    public function testListChildren()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart', array(\n                $this->createDirectory('/puli', array(\n                    $this->createFile('/.dotfile'),\n                    $this->createFile('/foo'),\n                    $this->createFile('/bar'),\n                    $this->createDirectory('/dir', array(\n                        // Nest another directory which matches the regex\n                        // /webmozart/puli/[^/]+$\n                        $this->createDirectory('/webmozart', array(\n                            $this->createDirectory('/puli', array(\n                                $this->createFile('/file'),\n                            )),\n                        )),\n                    )),\n                )),\n            )),\n        ))));\n\n        $resources = $repo->listChildren('/webmozart/puli');\n\n        $this->assertCount(4, $resources);\n        $this->assertInstanceOf('Puli\\Repository\\Api\\ResourceCollection', $resources);\n        // sorted\n        $this->assertEquals($repo->get('/webmozart/puli/.dotfile'), $resources[0]);\n        $this->assertEquals($repo->get('/webmozart/puli/bar'), $resources[1]);\n        $this->assertEquals($repo->get('/webmozart/puli/dir'), $resources[2]);\n        $this->assertEquals($repo->get('/webmozart/puli/foo'), $resources[3]);\n    }\n\n    public function testListChildrenReturnsEmptyCollectionForFiles()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart', array(\n                $this->createDirectory('/puli', array(\n                    $this->createFile('/foo'),\n                )),\n            )),\n        ))));\n\n        $resources = $repo->listChildren('/webmozart/puli/foo');\n\n        $this->assertCount(0, $resources);\n        $this->assertInstanceOf('Puli\\Repository\\Api\\ResourceCollection', $resources);\n    }\n\n    public function testListRoot()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart'),\n            $this->createDirectory('/acme'),\n        ))));\n\n        $resources = $repo->listChildren('/');\n\n        $this->assertCount(2, $resources);\n        $this->assertInstanceOf('Puli\\Repository\\Api\\ResourceCollection', $resources);\n        // sorted\n        $this->assertEquals($repo->get('/acme'), $resources[0]);\n        $this->assertEquals($repo->get('/webmozart'), $resources[1]);\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\ResourceNotFoundException\n     */\n    public function testListChildrenExpectsExistingResource()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/')));\n\n        $repo->listChildren('/foo/bar');\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testListChildrenExpectsAbsolutePath()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart'),\n        ))));\n\n        $repo->listChildren('webmozart');\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testListChildrenExpectsNonEmptyPath()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/')));\n\n        $repo->listChildren('');\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testListChildrenExpectsStringPath()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/')));\n\n        $repo->listChildren(new \\stdClass());\n    }\n\n    public function testFind()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart', array(\n                $this->createDirectory('/puli', array(\n                    $this->createFile('/.dotfoo'),\n                    $this->createFile('/foo'),\n                    $this->createFile('/bar'),\n                    $this->createDirectory('/dirfoo'),\n                )),\n            )),\n        ))));\n\n        $resources = $repo->find('/webmozart/**/*foo');\n\n        $this->assertCount(3, $resources);\n        $this->assertInstanceOf('Puli\\Repository\\Api\\ResourceCollection', $resources);\n        // sorted\n        $this->assertEquals($repo->get('/webmozart/puli/.dotfoo'), $resources[0]);\n        $this->assertEquals($repo->get('/webmozart/puli/dirfoo'), $resources[1]);\n        $this->assertEquals($repo->get('/webmozart/puli/foo'), $resources[2]);\n    }\n\n    public function testFindBrackets()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart', array(\n                $this->createDirectory('/puli', array(\n                    $this->createFile('/.dotfoo'),\n                    $this->createFile('/foo'),\n                    $this->createFile('/bar'),\n                    $this->createDirectory('/dirfoo'),\n                )),\n            )),\n        ))));\n\n        $resources = $repo->find('/webmozart/puli/{foo,bar}');\n\n        $this->assertCount(2, $resources);\n        $this->assertInstanceOf('Puli\\Repository\\Api\\ResourceCollection', $resources);\n        // sorted\n        $this->assertEquals($repo->get('/webmozart/puli/bar'), $resources[0]);\n        $this->assertEquals($repo->get('/webmozart/puli/foo'), $resources[1]);\n    }\n\n    public function testFindFull()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart', array(\n                $this->createDirectory('/puli', array(\n                    $this->createFile('/.dotfoo'),\n                    $this->createFile('/foo'),\n                    $this->createFile('/bar'),\n                    $this->createDirectory('/dirfoo'),\n                )),\n            )),\n        ))));\n\n        $resources = $repo->find('/webmozart/**/*{foo,bar}');\n\n        $this->assertCount(4, $resources);\n        $this->assertInstanceOf('Puli\\Repository\\Api\\ResourceCollection', $resources);\n        // sorted\n        $this->assertEquals($repo->get('/webmozart/puli/.dotfoo'), $resources[0]);\n        $this->assertEquals($repo->get('/webmozart/puli/bar'), $resources[1]);\n        $this->assertEquals($repo->get('/webmozart/puli/dirfoo'), $resources[2]);\n        $this->assertEquals($repo->get('/webmozart/puli/foo'), $resources[3]);\n    }\n\n    public function testFindFile()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart', array(\n                $this->createDirectory('/puli', array(\n                    $this->createFile('/file'),\n                )),\n            )),\n        ))));\n\n        $resources = $repo->find('/webmozart/puli/file');\n\n        $this->assertCount(1, $resources);\n        $this->assertInstanceOf('Puli\\Repository\\Api\\ResourceCollection', $resources);\n        $this->assertEquals($repo->get('/webmozart/puli/file'), $resources[0]);\n    }\n\n    public function testFindDirectory()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart'),\n        ))));\n\n        $resources = $repo->find('/webmozart');\n\n        $this->assertCount(1, $resources);\n        $this->assertInstanceOf('Puli\\Repository\\Api\\ResourceCollection', $resources);\n        $this->assertEquals($repo->get('/webmozart'), $resources[0]);\n    }\n\n    public function testFindCanonicalizesGlob()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart', array(\n                $this->createDirectory('/puli', array(\n                    $this->createFile('/file1'),\n                )),\n            )),\n        ))));\n\n        $resources = $repo->find('/webmozart/puli/../puli/./**');\n\n        $this->assertCount(1, $resources);\n        $this->assertInstanceOf('Puli\\Repository\\Api\\ResourceCollection', $resources);\n        $this->assertEquals($repo->get('/webmozart/puli/file1'), $resources[0]);\n    }\n\n    public function testFindNoMatches()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/')));\n\n        $resources = $repo->find('/foo/**');\n\n        $this->assertCount(0, $resources);\n        $this->assertInstanceOf('Puli\\Repository\\Api\\ResourceCollection', $resources);\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testFindExpectsAbsolutePath()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/')));\n\n        $repo->find('*');\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testFindExpectsNonEmptyPath()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/')));\n\n        $repo->find('');\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testFindExpectsStringPath()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/')));\n\n        $repo->find(new \\stdClass());\n    }\n\n    public function testGetVersions()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/', array(\n            $this->createDirectory('/webmozart', array(\n                $this->createDirectory('/puli', array(\n                    $this->createFile('/file1'),\n                    $this->createFile('/file2'),\n                )),\n            )),\n        ))));\n\n        $resource = $repo->get('/webmozart/puli/file1');\n        $versions = $repo->getVersions('/webmozart/puli/file1');\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\ChangeStream\\VersionList', $versions);\n        $this->assertEquals(array(0), $versions->getVersions());\n        $this->assertEquals($resource, $versions->getFirst());\n        $this->assertEquals($resource, $versions->getCurrent());\n        $this->assertEquals($resource, $versions->get(0));\n    }\n\n    public function testGetRootVersions()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory()));\n\n        $resource = $repo->get('/');\n        $versions = $repo->getVersions('/');\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\ChangeStream\\VersionList', $versions);\n        $this->assertEquals(array(0), $versions->getVersions());\n        $this->assertEquals($resource, $versions->getFirst());\n        $this->assertEquals($resource, $versions->getCurrent());\n        $this->assertEquals($resource, $versions->get(0));\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\NoVersionFoundException\n     */\n    public function testGetVersionsFailsIfNoneFound()\n    {\n        $repo = $this->createPrefilledRepository($this->prepareFixtures($this->createDirectory('/')));\n\n        $repo->getVersions('/foo/bar');\n    }\n}\n"
  },
  {
    "path": "tests/Api/ChangeStream/VersionListTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests\\Api\\ChangeStream;\n\nuse PHPUnit_Framework_MockObject_MockObject;\nuse PHPUnit_Framework_TestCase;\nuse Puli\\Repository\\Api\\ChangeStream\\VersionList;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\n\n/**\n * @author Titouan Galopin <galopintitouan@gmail.com>\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass VersionListTest extends PHPUnit_Framework_TestCase\n{\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testFailIfEmptyPath()\n    {\n        new VersionList('', array($this->getMockResource()));\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testFailIfInvalidPath()\n    {\n        new VersionList(1234, array($this->getMockResource()));\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testFailIfNoVersion()\n    {\n        new VersionList('/path', array());\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testFailIfInvalidVersions()\n    {\n        new VersionList('/path', array(new \\stdClass()));\n    }\n\n    public function testGetCurrentVersion()\n    {\n        $list = new VersionList('/path', array(\n            $this->getMockResource(),\n            $this->getMockResource(),\n            $this->getMockResource(),\n            $this->getMockResource(),\n        ));\n\n        $this->assertSame(3, $list->getCurrentVersion());\n    }\n\n    public function testGetCurrent()\n    {\n        $list = new VersionList('/path', array(\n            $this->getMockResource(),\n            $this->getMockResource(),\n            $this->getMockResource(),\n            $v4 = $this->getMockResource(),\n        ));\n\n        $this->assertSame($v4, $list->getCurrent());\n    }\n\n    public function testGetFirstVersion()\n    {\n        $list = new VersionList('/path', array(\n            $this->getMockResource(),\n            $this->getMockResource(),\n            $this->getMockResource(),\n            $this->getMockResource(),\n        ));\n\n        $this->assertSame(0, $list->getFirstVersion());\n    }\n\n    public function testGetFirst()\n    {\n        $list = new VersionList('/path', array(\n            $v1 = $this->getMockResource(),\n            $this->getMockResource(),\n            $this->getMockResource(),\n            $this->getMockResource(),\n        ));\n\n        $this->assertSame($v1, $list->getFirst());\n    }\n\n    public function testGet()\n    {\n        $list = new VersionList('/path', array(\n            $v1 = $this->getMockResource(),\n            $v2 = $this->getMockResource(),\n            $v3 = $this->getMockResource(),\n            $v4 = $this->getMockResource(),\n        ));\n\n        $this->assertSame($v1, $list->get(0));\n        $this->assertSame($v2, $list->get(1));\n        $this->assertSame($v3, $list->get(2));\n        $this->assertSame($v4, $list->get(3));\n    }\n\n    /**\n     * @expectedException \\OutOfBoundsException\n     */\n    public function testGetFailsIfNotFound()\n    {\n        $list = new VersionList('/path', array($this->getMockResource()));\n\n        $list->get(2);\n    }\n\n    public function testGetVersions()\n    {\n        $list = new VersionList('/path', array(\n            $this->getMockResource(),\n            $this->getMockResource(),\n            $this->getMockResource(),\n            $this->getMockResource(),\n        ));\n\n        $this->assertSame(array(0, 1, 2, 3), $list->getVersions());\n    }\n\n    public function testCount()\n    {\n        $list = new VersionList('/path', array(\n            $this->getMockResource(),\n            $this->getMockResource(),\n            $this->getMockResource(),\n            $this->getMockResource(),\n        ));\n\n        $this->assertCount(4, $list);\n    }\n\n    public function testIterate()\n    {\n        $list = new VersionList('/path', array(\n            $v1 = $this->getMockResource(),\n            $v2 = $this->getMockResource(),\n            $v3 = $this->getMockResource(),\n            $v4 = $this->getMockResource(),\n        ));\n\n        $this->assertSame(array($v1, $v2, $v3, $v4), iterator_to_array($list));\n    }\n\n    public function testToArray()\n    {\n        $list = new VersionList('/path', array(\n            $v1 = $this->getMockResource(),\n            $v2 = $this->getMockResource(),\n            $v3 = $this->getMockResource(),\n            $v4 = $this->getMockResource(),\n        ));\n\n        $this->assertSame(array($v1, $v2, $v3, $v4), $list->toArray());\n    }\n\n    public function testArrayAccess()\n    {\n        $list = new VersionList('/path', array(\n            $v1 = $this->getMockResource(),\n            $v2 = $this->getMockResource(),\n            $v3 = $this->getMockResource(),\n            $v4 = $this->getMockResource(),\n        ));\n\n        $this->assertSame($v1, $list[0]);\n        $this->assertSame($v2, $list[1]);\n        $this->assertSame($v3, $list[2]);\n        $this->assertSame($v4, $list[3]);\n        $this->assertTrue(isset($list[0]));\n        $this->assertFalse(isset($list[4]));\n    }\n\n    /**\n     * @return PHPUnit_Framework_MockObject_MockObject|PuliResource\n     */\n    private function getMockResource()\n    {\n        return $this->getMock('Puli\\Repository\\Api\\Resource\\PuliResource');\n    }\n}\n"
  },
  {
    "path": "tests/ChangeStream/AbstractChangeStreamTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests\\ChangeStream;\n\nuse PHPUnit_Framework_TestCase;\nuse Puli\\Repository\\Api\\ChangeStream\\ChangeStream;\nuse Puli\\Repository\\InMemoryRepository;\nuse Puli\\Repository\\Resource\\FileResource;\n\n/**\n * @author Titouan Galopin <galopintitouan@gmail.com>\n */\nabstract class AbstractChangeStreamTest extends PHPUnit_Framework_TestCase\n{\n    /**\n     * @var InMemoryRepository\n     */\n    protected $repo;\n\n    /**\n     * @var ChangeStream\n     */\n    protected $writeStream;\n\n    /**\n     * @var ChangeStream\n     */\n    protected $readStream;\n\n    /**\n     * @var string\n     */\n    protected $fixtureDir;\n\n    protected function setUp()\n    {\n        $this->repo = new InMemoryRepository();\n        $this->writeStream = $this->createWriteStream();\n        $this->readStream = $this->createReadStream($this->writeStream);\n        $this->fixtureDir = __DIR__.'/../Fixtures';\n    }\n\n    /**\n     * @return ChangeStream\n     */\n    abstract protected function createWriteStream();\n\n    /**\n     * @param ChangeStream $writeStream\n     *\n     * @return ChangeStream\n     */\n    abstract protected function createReadStream(ChangeStream $writeStream);\n\n    public function testAppend()\n    {\n        $this->writeStream->append($v1 = new FileResource($this->fixtureDir.'/dir1/file1', '/path'));\n        $this->writeStream->append($v2 = new FileResource($this->fixtureDir.'/dir1/file2', '/path'));\n        $this->writeStream->append($v3 = new FileResource($this->fixtureDir.'/dir2/file2', '/path'));\n\n        $versions = $this->readStream->getVersions('/path');\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\ChangeStream\\VersionList', $versions);\n        $this->assertSame('/path', $versions->getPath());\n        $this->assertEquals(array(0, 1, 2), $versions->getVersions());\n        $this->assertEquals($v1, $versions->get(0));\n        $this->assertEquals($v2, $versions->get(1));\n        $this->assertEquals($v3, $versions->get(2));\n    }\n\n    public function testContains()\n    {\n        $this->assertFalse($this->readStream->contains('/path'));\n\n        $this->writeStream->append(new FileResource($this->fixtureDir.'/dir1/file1', '/path'));\n\n        $this->readStream = $this->createReadStream($this->writeStream);\n\n        $this->assertTrue($this->readStream->contains('/path'));\n    }\n\n    public function testPurge()\n    {\n        $this->writeStream->append(new FileResource($this->fixtureDir.'/dir1/file1', '/path1'));\n        $this->writeStream->append(new FileResource($this->fixtureDir.'/dir1/file2', '/path1'));\n        $this->writeStream->append(new FileResource($this->fixtureDir.'/dir2/file2', '/path2'));\n\n        $this->writeStream->purge('/path1');\n\n        $this->assertFalse($this->readStream->contains('/path1'));\n        $this->assertTrue($this->readStream->contains('/path2'));\n    }\n\n    public function testClear()\n    {\n        $this->writeStream->append(new FileResource($this->fixtureDir.'/dir1/file1', '/path1'));\n        $this->writeStream->append(new FileResource($this->fixtureDir.'/dir1/file2', '/path1'));\n        $this->writeStream->append(new FileResource($this->fixtureDir.'/dir2/file2', '/path2'));\n\n        $this->writeStream->clear();\n\n        $this->assertFalse($this->readStream->contains('/path1'));\n        $this->assertFalse($this->readStream->contains('/path2'));\n    }\n\n    public function testAppendAfterPurging()\n    {\n        $this->writeStream->append(new FileResource($this->fixtureDir.'/dir1/file1', '/path'));\n\n        $this->writeStream->purge('/path');\n\n        $this->writeStream->append(new FileResource($this->fixtureDir.'/dir1/file2', '/path'));\n\n        $this->assertTrue($this->readStream->contains('/path'));\n        $this->assertCount(1, $this->readStream->getVersions('/path'));\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\NoVersionFoundException\n     */\n    public function testGetVersionsFailsIfNotFound()\n    {\n        $this->readStream->getVersions('/foobar');\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\NoVersionFoundException\n     */\n    public function testGetVersionsFailsAfterPurging()\n    {\n        $this->writeStream->append(new FileResource($this->fixtureDir.'/dir1/file1', '/path'));\n        $this->writeStream->append(new FileResource($this->fixtureDir.'/dir1/file2', '/path'));\n\n        $this->writeStream->purge('/path');\n\n        $this->readStream->getVersions('/path');\n    }\n\n    public function testResourcesNotAttachedToRepositoryByDefault()\n    {\n        $this->writeStream->append(new FileResource($this->fixtureDir.'/dir1/file1', '/path'));\n        $this->writeStream->append(new FileResource($this->fixtureDir.'/dir1/file2', '/path'));\n\n        $versions = $this->readStream->getVersions('/path');\n\n        $this->assertCount(2, $versions);\n        $this->assertNull($versions->get(0)->getRepository());\n        $this->assertNull($versions->get(1)->getRepository());\n    }\n\n    public function testResourcesAttachedToRepositoryIfPassed()\n    {\n        $this->writeStream->append(new FileResource($this->fixtureDir.'/dir1/file1', '/path'));\n        $this->writeStream->append(new FileResource($this->fixtureDir.'/dir1/file2', '/path'));\n\n        $versions = $this->readStream->getVersions('/path', $this->repo);\n\n        $this->assertCount(2, $versions);\n        $this->assertSame($this->repo, $versions->get(0)->getRepository());\n        $this->assertSame($this->repo, $versions->get(1)->getRepository());\n    }\n\n    public function testResourcesInStreamRemainDetached()\n    {\n        $this->writeStream->append(new FileResource($this->fixtureDir.'/dir1/file1', '/path'));\n        $this->writeStream->append(new FileResource($this->fixtureDir.'/dir1/file2', '/path'));\n\n        // attached versions\n        $this->readStream->getVersions('/path', $this->repo);\n\n        // still detached (clones)\n        $versions = $this->readStream->getVersions('/path');\n\n        $this->assertCount(2, $versions);\n        $this->assertNull($versions->get(0)->getRepository());\n        $this->assertNull($versions->get(1)->getRepository());\n    }\n}\n"
  },
  {
    "path": "tests/ChangeStream/InMemoryChangeStreamTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests\\ChangeStream;\n\nuse Puli\\Repository\\Api\\ChangeStream\\ChangeStream;\nuse Puli\\Repository\\ChangeStream\\InMemoryChangeStream;\n\n/**\n * @author Titouan Galopin <galopintitouan@gmail.com>\n */\nclass InMemoryChangeStreamTest extends AbstractChangeStreamTest\n{\n    protected function createWriteStream()\n    {\n        return new InMemoryChangeStream();\n    }\n\n    protected function createReadStream(ChangeStream $writeStream)\n    {\n        return $writeStream;\n    }\n}\n"
  },
  {
    "path": "tests/ChangeStream/JsonChangeStreamLoadedTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests\\ChangeStream;\n\nuse Puli\\Repository\\Api\\ChangeStream\\ChangeStream;\n\n/**\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass JsonChangeStreamLoadedTest extends JsonChangeStreamTest\n{\n    protected function createReadStream(ChangeStream $writeStream)\n    {\n        return $writeStream;\n    }\n}\n"
  },
  {
    "path": "tests/ChangeStream/JsonChangeStreamTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests\\ChangeStream;\n\nuse Puli\\Repository\\Api\\ChangeStream\\ChangeStream;\nuse Puli\\Repository\\ChangeStream\\JsonChangeStream;\nuse Webmozart\\Glob\\Test\\TestUtil;\n\n/**\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass JsonChangeStreamTest extends AbstractChangeStreamTest\n{\n    /**\n     * @var string\n     */\n    private $tempDir;\n\n    /**\n     * @var string\n     */\n    private $path;\n\n    protected function setUp()\n    {\n        $this->tempDir = TestUtil::makeTempDir('puli-repository', __CLASS__);\n        $this->path = $this->tempDir.'/change-stream.json';\n\n        parent::setUp();\n    }\n\n    protected function createWriteStream()\n    {\n        return new JsonChangeStream($this->path);\n    }\n\n    protected function createReadStream(ChangeStream $writeStream)\n    {\n        return new JsonChangeStream($this->path);\n    }\n}\n"
  },
  {
    "path": "tests/ChangeStream/KeyValueStoreChangeStreamTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests\\ChangeStream;\n\nuse Puli\\Repository\\Api\\ChangeStream\\ChangeStream;\nuse Puli\\Repository\\ChangeStream\\KeyValueStoreChangeStream;\nuse Webmozart\\KeyValueStore\\ArrayStore;\n\n/**\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass KeyValueStoreChangeStreamTest extends AbstractChangeStreamTest\n{\n    /**\n     * @var ArrayStore\n     */\n    private $store;\n\n    protected function setUp()\n    {\n        $this->store = new ArrayStore();\n\n        parent::setUp();\n    }\n\n    protected function createWriteStream()\n    {\n        return new KeyValueStoreChangeStream($this->store);\n    }\n\n    /**\n     * @param ChangeStream $writeStream\n     *\n     * @return ChangeStream\n     */\n    protected function createReadStream(ChangeStream $writeStream)\n    {\n        return new KeyValueStoreChangeStream($this->store);\n    }\n}\n"
  },
  {
    "path": "tests/Discovery/Fixtures/SubResourceBinding.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests\\Discovery\\Fixtures;\n\nuse Puli\\Repository\\Discovery\\ResourceBinding;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass SubResourceBinding extends ResourceBinding\n{\n}\n"
  },
  {
    "path": "tests/Discovery/ResourceBindingInitializerTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests\\Discovery;\n\nuse PHPUnit_Framework_MockObject_MockObject;\nuse PHPUnit_Framework_TestCase;\nuse Puli\\Discovery\\Binding\\ClassBinding;\nuse Puli\\Repository\\Api\\ResourceRepository;\nuse Puli\\Repository\\Discovery\\ResourceBinding;\nuse Puli\\Repository\\Discovery\\ResourceBindingInitializer;\n\n/**\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass ResourceBindingInitializerTest extends PHPUnit_Framework_TestCase\n{\n    const RESOURCE_BINDING = 'Puli\\Repository\\Discovery\\ResourceBinding';\n\n    const SUB_RESOURCE_BINDING = 'Puli\\Repository\\Tests\\Discovery\\Fixtures\\SubResourceBinding';\n\n    const CLASS_BINDING = 'Puli\\Discovery\\Binding\\ClassBinding';\n\n    /**\n     * @var PHPUnit_Framework_MockObject_MockObject|ResourceRepository\n     */\n    private $repo;\n\n    /**\n     * @var ResourceBindingInitializer\n     */\n    private $initializer;\n\n    protected function setUp()\n    {\n        $this->repo = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n        $this->initializer = new ResourceBindingInitializer($this->repo);\n    }\n\n    public function testAcceptsBinding()\n    {\n        $this->assertTrue($this->initializer->acceptsBinding(self::RESOURCE_BINDING));\n        $this->assertFalse($this->initializer->acceptsBinding(self::CLASS_BINDING));\n        $this->assertTrue($this->initializer->acceptsBinding(new ResourceBinding('/path', 'acme/foo')));\n        $this->assertFalse($this->initializer->acceptsBinding(new ClassBinding(__CLASS__, 'acme/foo')));\n    }\n\n    public function testAcceptsBindingAcceptsSubClasses()\n    {\n        $this->assertTrue($this->initializer->acceptsBinding(self::SUB_RESOURCE_BINDING));\n    }\n\n    public function testGetAcceptedBindingClass()\n    {\n        $this->assertSame(self::RESOURCE_BINDING, $this->initializer->getAcceptedBindingClass());\n    }\n\n    public function testInitializeBinding()\n    {\n        $binding = $this->getMockBuilder(self::RESOURCE_BINDING)\n            ->disableOriginalConstructor()\n            ->getMock();\n\n        $binding->expects($this->once())\n            ->method('setRepository')\n            ->with($this->repo);\n\n        $this->initializer->initializeBinding($binding);\n    }\n\n    public function testInitializeBindingOfSubClass()\n    {\n        $binding = $this->getMockBuilder(self::SUB_RESOURCE_BINDING)\n            ->disableOriginalConstructor()\n            ->getMock();\n\n        $binding->expects($this->once())\n            ->method('setRepository')\n            ->with($this->repo);\n\n        $this->initializer->initializeBinding($binding);\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testInitializeBindingFailsIfInvalidArgument()\n    {\n        $this->initializer->initializeBinding(new ClassBinding(__CLASS__, 'acme/foo'));\n    }\n}\n"
  },
  {
    "path": "tests/Discovery/ResourceBindingTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests\\Discovery;\n\nuse Puli\\Discovery\\Test\\AbstractBindingTest;\nuse Puli\\Repository\\Discovery\\ResourceBinding;\n\n/**\n * @since  1.0\n *\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass ResourceBindingTest extends AbstractBindingTest\n{\n    protected function createBinding($typeName, array $parameterValues = array())\n    {\n        return new ResourceBinding('/path/*', $typeName, $parameterValues, 'glob');\n    }\n\n    public function testCreateWithQuery()\n    {\n        $binding = new ResourceBinding('/path/*', 'acme/foo', array(), 'glob');\n\n        $this->assertSame('/path/*', $binding->getQuery());\n        $this->assertSame('glob', $binding->getLanguage());\n        $this->assertSame('acme/foo', $binding->getTypeName());\n    }\n\n    public function testGetResources()\n    {\n        $repo = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n        $binding = new ResourceBinding('/path/*', 'acme/foo', array(), 'language');\n\n        $repo->expects($this->once())\n            ->method('find')\n            ->with('/path/*', 'language')\n            ->willReturn('RESULT');\n\n        $binding->setRepository($repo);\n\n        $this->assertSame('RESULT', $binding->getResources());\n    }\n\n    /**\n     * @expectedException \\Puli\\Discovery\\Api\\Binding\\Initializer\\NotInitializedException\n     */\n    public function testGetResourcesFailsIfNotSet()\n    {\n        $binding = new ResourceBinding('/path/*', 'acme/foo', array(), 'language');\n\n        $binding->getResources();\n    }\n}\n"
  },
  {
    "path": "tests/FilesystemRepositoryAbsoluteSymlinkTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests;\n\nuse Puli\\Repository\\Api\\EditableRepository;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\nuse Puli\\Repository\\FilesystemRepository;\nuse Puli\\Repository\\Resource\\DirectoryResource;\nuse Puli\\Repository\\Resource\\FileResource;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass FilesystemRepositoryAbsoluteSymlinkTest extends AbstractFilesystemRepositorySymlinkTest\n{\n    protected function createPrefilledRepository(PuliResource $root)\n    {\n        $repo = new FilesystemRepository($this->tempDir, true, false);\n        $repo->add('/', $root);\n\n        return $repo;\n    }\n\n    protected function createWriteRepository()\n    {\n        return new FilesystemRepository($this->tempDir, true, false, $this->stream);\n    }\n\n    protected function createReadRepository(EditableRepository $writeRepo)\n    {\n        return new FilesystemRepository($this->tempDir, true, false, $this->stream);\n    }\n\n    public function testAddDirectoryCreatesSymlink()\n    {\n        $this->writeRepo->add('/webmozart/dir', new DirectoryResource($this->tempFixtures.'/dir1'));\n\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/dir'));\n        $this->assertPathsAreEqual($this->tempFixtures.'/dir1', readlink($this->tempDir.'/webmozart/dir'));\n    }\n\n    public function testOverwriteDirectoryWithDirectoryTurnsSymlinkIntoDirectory()\n    {\n        $this->writeRepo->add('/webmozart/dir', new DirectoryResource($this->tempFixtures.'/dir1'));\n        $this->writeRepo->add('/webmozart/dir', new DirectoryResource($this->tempFixtures.'/dir2'));\n\n        // Symlink is turned into a copy\n        $this->assertFalse(is_link($this->tempDir.'/webmozart/dir'));\n\n        // Directories are merged\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/dir/file1'));\n        $this->assertPathsAreEqual($this->tempFixtures.'/dir1/file1', readlink($this->tempDir.'/webmozart/dir/file1'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/dir/file2'));\n        $this->assertPathsAreEqual($this->tempFixtures.'/dir2/file2', readlink($this->tempDir.'/webmozart/dir/file2'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/dir/file3'));\n        $this->assertPathsAreEqual($this->tempFixtures.'/dir2/file3', readlink($this->tempDir.'/webmozart/dir/file3'));\n    }\n\n    public function testOverwriteDirectoryWithDirectoryMergesSubdirectories()\n    {\n        $this->writeRepo->add('/webmozart/dir', new DirectoryResource($this->tempFixtures.'/dir3'));\n        $this->writeRepo->add('/webmozart/dir', new DirectoryResource($this->tempFixtures.'/dir4'));\n\n        // Symlink is turned into a copy\n        $this->assertFalse(is_link($this->tempDir.'/webmozart/dir'));\n\n        // Subdirectories are merged\n        $this->assertTrue(is_dir($this->tempDir.'/webmozart/dir/sub'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/dir/sub/file1'));\n        $this->assertPathsAreEqual($this->tempFixtures.'/dir3/sub/file1', readlink($this->tempDir.'/webmozart/dir/sub/file1'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/dir/sub/file2'));\n        $this->assertPathsAreEqual($this->tempFixtures.'/dir4/sub/file2', readlink($this->tempDir.'/webmozart/dir/sub/file2'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/dir/sub/file3'));\n        $this->assertPathsAreEqual($this->tempFixtures.'/dir4/sub/file3', readlink($this->tempDir.'/webmozart/dir/sub/file3'));\n    }\n\n    public function testOverwriteDirectoryWithFileReplacesSymlink()\n    {\n        $this->writeRepo->add('/webmozart/path', new DirectoryResource($this->tempFixtures.'/dir1'));\n        $this->writeRepo->add('/webmozart/path', new FileResource($this->tempFixtures.'/dir1/file1'));\n\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/path'));\n        $this->assertPathsAreEqual($this->tempFixtures.'/dir1/file1', readlink($this->tempDir.'/webmozart/path'));\n    }\n\n    public function testAddFileCreatesSymlink()\n    {\n        $this->writeRepo->add('/webmozart/file', new FileResource($this->tempFixtures.'/dir1/file2'));\n\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/file'));\n        $this->assertPathsAreEqual($this->tempFixtures.'/dir1/file2', readlink($this->tempDir.'/webmozart/file'));\n    }\n\n    public function testOverwriteFileWithDirectoryReplacesSymlink()\n    {\n        $this->writeRepo->add('/webmozart/path', new FileResource($this->tempFixtures.'/dir1/file2'));\n        $this->writeRepo->add('/webmozart/path', new DirectoryResource($this->tempFixtures.'/dir1'));\n\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/path'));\n        $this->assertPathsAreEqual($this->tempFixtures.'/dir1', readlink($this->tempDir.'/webmozart/path'));\n    }\n\n    public function testOverwriteFileWithFileReplacesSymlink()\n    {\n        $this->writeRepo->add('/webmozart/path', new FileResource($this->tempFixtures.'/dir1/file2'));\n        $this->writeRepo->add('/webmozart/path', new FileResource($this->tempFixtures.'/dir1/file1'));\n\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/path'));\n        $this->assertPathsAreEqual($this->tempFixtures.'/dir1/file1', readlink($this->tempDir.'/webmozart/path'));\n    }\n\n    public function testAddSubDirectoryTurnsParentSymlinkIntoDirectory()\n    {\n        $this->writeRepo->add('/webmozart', new DirectoryResource($this->tempFixtures.'/dir1'));\n        $this->writeRepo->add('/webmozart/dir', new DirectoryResource($this->tempFixtures.'/dir2'));\n\n        // Symlink is turned into a copy\n        $this->assertFalse(is_link($this->tempDir.'/webmozart'));\n\n        // Directories are merged\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/file1'));\n        $this->assertPathsAreEqual($this->tempFixtures.'/dir1/file1', readlink($this->tempDir.'/webmozart/file1'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/file2'));\n        $this->assertPathsAreEqual($this->tempFixtures.'/dir1/file2', readlink($this->tempDir.'/webmozart/file2'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/dir'));\n        $this->assertPathsAreEqual($this->tempFixtures.'/dir2', readlink($this->tempDir.'/webmozart/dir'));\n    }\n\n    public function testAddSubFileTurnsParentSymlinkIntoDirectory()\n    {\n        $this->writeRepo->add('/webmozart', new DirectoryResource($this->tempFixtures.'/dir1'));\n        $this->writeRepo->add('/webmozart/file3', new FileResource($this->tempFixtures.'/dir2/file3'));\n\n        // Symlink is turned into a copy\n        $this->assertFalse(is_link($this->tempDir.'/webmozart'));\n\n        // Directories are merged\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/file1'));\n        $this->assertPathsAreEqual($this->tempFixtures.'/dir1/file1', readlink($this->tempDir.'/webmozart/file1'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/file2'));\n        $this->assertPathsAreEqual($this->tempFixtures.'/dir1/file2', readlink($this->tempDir.'/webmozart/file2'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/file3'));\n        $this->assertPathsAreEqual($this->tempFixtures.'/dir2/file3', readlink($this->tempDir.'/webmozart/file3'));\n    }\n\n    public function testAddSubResourceWithBodyTurnsParentSymlinkIntoDirectory()\n    {\n        $this->writeRepo->add('/webmozart', new DirectoryResource($this->tempFixtures.'/dir1'));\n        $this->writeRepo->add('/webmozart/file3', $this->createFile(null, 'some body'));\n\n        // Symlink is turned into a copy\n        $this->assertFalse(is_link($this->tempDir.'/webmozart'));\n\n        // Directories are merged\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/file1'));\n        $this->assertPathsAreEqual($this->tempFixtures.'/dir1/file1', readlink($this->tempDir.'/webmozart/file1'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/file2'));\n        $this->assertPathsAreEqual($this->tempFixtures.'/dir1/file2', readlink($this->tempDir.'/webmozart/file2'));\n        $this->assertFalse(is_link($this->tempDir.'/webmozart/file3'));\n        $this->assertPathsAreEqual('some body', file_get_contents($this->tempDir.'/webmozart/file3'));\n    }\n\n    public function testAddSubSubDirectoryTurnsParentSymlinkIntoDirectory()\n    {\n        $this->writeRepo->add('/webmozart', new DirectoryResource($this->tempFixtures.'/dir3'));\n        $this->writeRepo->add('/webmozart/sub/dir', new DirectoryResource($this->tempFixtures.'/dir1'));\n\n        // Symlink is turned into a copy\n        $this->assertFalse(is_link($this->tempDir.'/webmozart'));\n\n        // Directories are merged\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/sub/file1'));\n        $this->assertPathsAreEqual($this->tempFixtures.'/dir3/sub/file1', readlink($this->tempDir.'/webmozart/sub/file1'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/sub/file2'));\n        $this->assertPathsAreEqual($this->tempFixtures.'/dir3/sub/file2', readlink($this->tempDir.'/webmozart/sub/file2'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/sub/dir'));\n        $this->assertPathsAreEqual($this->tempFixtures.'/dir1', readlink($this->tempDir.'/webmozart/sub/dir'));\n    }\n\n    public function testAddSubSubFileTurnsParentSymlinkIntoDirectory()\n    {\n        $this->writeRepo->add('/webmozart', new DirectoryResource($this->tempFixtures.'/dir3'));\n        $this->writeRepo->add('/webmozart/sub/file3', new FileResource($this->tempFixtures.'/dir2/file3'));\n\n        // Symlink is turned into a copy\n        $this->assertFalse(is_link($this->tempDir.'/webmozart'));\n\n        // Directories are merged\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/sub/file1'));\n        $this->assertPathsAreEqual($this->tempFixtures.'/dir3/sub/file1', readlink($this->tempDir.'/webmozart/sub/file1'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/sub/file2'));\n        $this->assertPathsAreEqual($this->tempFixtures.'/dir3/sub/file2', readlink($this->tempDir.'/webmozart/sub/file2'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/sub/file3'));\n        $this->assertPathsAreEqual($this->tempFixtures.'/dir2/file3', readlink($this->tempDir.'/webmozart/sub/file3'));\n    }\n\n    public function testAddSubSubResourceWithBodyTurnsParentSymlinkIntoDirectory()\n    {\n        $this->writeRepo->add('/webmozart', new DirectoryResource($this->tempFixtures.'/dir3'));\n        $this->writeRepo->add('/webmozart/sub/file3', $this->createFile(null, 'some body'));\n\n        // Symlink is turned into a copy\n        $this->assertFalse(is_link($this->tempDir.'/webmozart'));\n\n        // Directories are merged\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/sub/file1'));\n        $this->assertPathsAreEqual($this->tempFixtures.'/dir3/sub/file1', readlink($this->tempDir.'/webmozart/sub/file1'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/sub/file2'));\n        $this->assertPathsAreEqual($this->tempFixtures.'/dir3/sub/file2', readlink($this->tempDir.'/webmozart/sub/file2'));\n        $this->assertFalse(is_link($this->tempDir.'/webmozart/sub/file3'));\n        $this->assertSame('some body', file_get_contents($this->tempDir.'/webmozart/sub/file3'));\n    }\n}\n"
  },
  {
    "path": "tests/FilesystemRepositoryCopyTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests;\n\nuse Puli\\Repository\\Api\\EditableRepository;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\nuse Puli\\Repository\\FilesystemRepository;\nuse Puli\\Repository\\Resource\\DirectoryResource;\nuse Puli\\Repository\\Resource\\FileResource;\nuse Puli\\Repository\\Resource\\LinkResource;\nuse Symfony\\Component\\Filesystem\\Filesystem;\nuse Webmozart\\Glob\\Test\\TestUtil;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass FilesystemRepositoryCopyTest extends AbstractEditableRepositoryTest\n{\n    private $tempDir;\n\n    protected function setUp()\n    {\n        $this->tempDir = TestUtil::makeTempDir('puli-repository', __CLASS__);\n\n        parent::setUp();\n    }\n\n    protected function tearDown()\n    {\n        parent::tearDown();\n\n        $filesystem = new Filesystem();\n        $filesystem->remove($this->tempDir);\n    }\n\n    protected function createPrefilledRepository(PuliResource $root)\n    {\n        $repo = new FilesystemRepository($this->tempDir, false);\n        $repo->add('/', $root);\n\n        return $repo;\n    }\n\n    protected function createWriteRepository()\n    {\n        return new FilesystemRepository($this->tempDir, false, true, $this->stream);\n    }\n\n    protected function createReadRepository(EditableRepository $writeRepo)\n    {\n        return new FilesystemRepository($this->tempDir, true, false, $this->stream);\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testPassNonExistingBaseDirectory()\n    {\n        new FilesystemRepository($this->tempDir.'/foo');\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testPassFileAsBaseDirectory()\n    {\n        touch($this->tempDir.'/file');\n\n        new FilesystemRepository($this->tempDir.'/file');\n    }\n\n    public function testGetFileLink()\n    {\n        $this->markAsSkippedIfSymlinkIsMissing();\n\n        touch($this->tempDir.'/file');\n        symlink($this->tempDir.'/file', $this->tempDir.'/link');\n\n        $expected = new LinkResource('/file', '/link');\n        $expected->attachTo($this->writeRepo);\n\n        $this->assertEquals($expected, $this->writeRepo->get('/link'));\n    }\n\n    public function testGetDirectoryLink()\n    {\n        $this->markAsSkippedIfSymlinkIsMissing();\n\n        mkdir($this->tempDir.'/dir');\n        symlink($this->tempDir.'/dir', $this->tempDir.'/link');\n\n        $expected = new LinkResource('/dir', '/link');\n        $expected->attachTo($this->writeRepo);\n\n        $this->assertEquals($expected, $this->writeRepo->get('/link'));\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedLanguageException\n     * @expectedExceptionMessage foobar\n     */\n    public function testContainsFailsIfLanguageNotGlob()\n    {\n        $this->readRepo->contains('/*', 'foobar');\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedLanguageException\n     * @expectedExceptionMessage foobar\n     */\n    public function testFindFailsIfLanguageNotGlob()\n    {\n        $this->readRepo->find('/*', 'foobar');\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedResourceException\n     */\n    public function testFailIfAddedResourceHasBodyAndChildren()\n    {\n        $resource = $this->getMock('Puli\\Repository\\Api\\Resource\\BodyResource');\n\n        $resource->expects($this->any())\n            ->method('hasChildren')\n            ->will($this->returnValue(true));\n\n        $this->writeRepo->add('/webmozart', $resource);\n    }\n\n    public function testAddDirectory()\n    {\n        $this->writeRepo->add('/webmozart/dir', new DirectoryResource(__DIR__.'/Fixtures/dir1'));\n\n        $dir = $this->readRepo->get('/webmozart/dir');\n        $file1 = $this->readRepo->get('/webmozart/dir/file1');\n        $file2 = $this->readRepo->get('/webmozart/dir/file2');\n\n        $this->assertInstanceOf('Puli\\Repository\\Resource\\DirectoryResource', $dir);\n        $this->assertInstanceOf('Puli\\Repository\\Resource\\FileResource', $file1);\n        $this->assertInstanceOf('Puli\\Repository\\Resource\\FileResource', $file2);\n    }\n\n    public function testAddFile()\n    {\n        $this->writeRepo->add('/webmozart/file', new FileResource(__DIR__.'/Fixtures/dir1/file2'));\n\n        $file = $this->readRepo->get('/webmozart/file');\n\n        $this->assertInstanceOf('Puli\\Repository\\Resource\\FileResource', $file);\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedOperationException\n     */\n    public function testFailIfAddingFileAsChildOfFile()\n    {\n        $this->writeRepo->add('/webmozart/puli', new FileResource(__DIR__.'/Fixtures/dir1/file1'));\n        $this->writeRepo->add('/webmozart/puli/file', new FileResource(__DIR__.'/Fixtures/dir1/file2'));\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedLanguageException\n     * @expectedExceptionMessage foobar\n     */\n    public function testRemoveFailsIfLanguageNotGlob()\n    {\n        $this->writeRepo->remove('/*', 'foobar');\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedResourceException\n     */\n    public function testFileLink()\n    {\n        $this->writeRepo->add('/webmozart/link', new LinkResource('/webmozart/puli/file'));\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedResourceException\n     */\n    public function testDirectoryLink()\n    {\n        $this->writeRepo->add('/webmozart/link', new LinkResource('/webmozart/puli/file'));\n    }\n}\n"
  },
  {
    "path": "tests/FilesystemRepositoryLoadedTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests;\n\nuse Puli\\Repository\\Api\\EditableRepository;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\nuse Puli\\Repository\\FilesystemRepository;\nuse Puli\\Repository\\Resource\\LinkResource;\nuse Symfony\\Component\\Filesystem\\Filesystem;\nuse Webmozart\\Glob\\Test\\TestUtil;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass FilesystemRepositoryLoadedTest extends AbstractEditableRepositoryTest\n{\n    private $tempDir;\n\n    protected function setUp()\n    {\n        $this->tempDir = TestUtil::makeTempDir('puli-repository', __CLASS__);\n\n        parent::setUp();\n    }\n\n    protected function tearDown()\n    {\n        parent::tearDown();\n\n        $filesystem = new Filesystem();\n        $filesystem->remove($this->tempDir);\n    }\n\n    protected function createPrefilledRepository(PuliResource $root)\n    {\n        $repo = new FilesystemRepository($this->tempDir, false);\n        $repo->add('/', $root);\n\n        return $repo;\n    }\n\n    protected function createWriteRepository()\n    {\n        return new FilesystemRepository($this->tempDir, false, true, $this->stream);\n    }\n\n    protected function createReadRepository(EditableRepository $writeRepo)\n    {\n        return $writeRepo;\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedResourceException\n     */\n    public function testFileLink()\n    {\n        $this->writeRepo->add('/webmozart/link', new LinkResource('/webmozart/puli/file'));\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedResourceException\n     */\n    public function testDirectoryLink()\n    {\n        $this->writeRepo->add('/webmozart/link', new LinkResource('/webmozart/puli/file'));\n    }\n}\n"
  },
  {
    "path": "tests/FilesystemRepositoryRelativeSymlinkTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests;\n\nuse Puli\\Repository\\Api\\EditableRepository;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\nuse Puli\\Repository\\FilesystemRepository;\nuse Puli\\Repository\\Resource\\DirectoryResource;\nuse Puli\\Repository\\Resource\\FileResource;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass FilesystemRepositoryRelativeSymlinkTest extends AbstractFilesystemRepositorySymlinkTest\n{\n    protected function setUp()\n    {\n        if (defined('PHP_WINDOWS_VERSION_MAJOR')) {\n            $this->markTestSkipped('Relative symbolic links are not supported on Windows.');\n        }\n\n        parent::setUp();\n    }\n\n    protected function createPrefilledRepository(PuliResource $root)\n    {\n        $repo = new FilesystemRepository($this->tempDir, true, true);\n        $repo->add('/', $root);\n\n        return $repo;\n    }\n\n    protected function createWriteRepository()\n    {\n        return new FilesystemRepository($this->tempDir, true, true, $this->stream);\n    }\n\n    protected function createReadRepository(EditableRepository $writeRepo)\n    {\n        return new FilesystemRepository($this->tempDir, true, false, $this->stream);\n    }\n\n    public function testAddDirectoryCreatesSymlink()\n    {\n        $this->writeRepo->add('/webmozart/dir', new DirectoryResource($this->tempFixtures.'/dir1'));\n\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/dir'));\n        $this->assertSame('../../fixtures/dir1', readlink($this->tempDir.'/webmozart/dir'));\n    }\n\n    public function testOverwriteDirectoryWithDirectoryTurnsSymlinkIntoDirectory()\n    {\n        $this->writeRepo->add('/webmozart/dir', new DirectoryResource($this->tempFixtures.'/dir1'));\n        $this->writeRepo->add('/webmozart/dir', new DirectoryResource($this->tempFixtures.'/dir2'));\n\n        // Symlink is turned into a copy\n        $this->assertFalse(is_link($this->tempDir.'/webmozart/dir'));\n\n        // Directories are merged\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/dir/file1'));\n        $this->assertSame('../../../fixtures/dir1/file1', readlink($this->tempDir.'/webmozart/dir/file1'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/dir/file2'));\n        $this->assertSame('../../../fixtures/dir2/file2', readlink($this->tempDir.'/webmozart/dir/file2'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/dir/file3'));\n        $this->assertSame('../../../fixtures/dir2/file3', readlink($this->tempDir.'/webmozart/dir/file3'));\n    }\n\n    public function testOverwriteDirectoryWithDirectoryMergesSubdirectories()\n    {\n        $this->writeRepo->add('/webmozart/dir', new DirectoryResource($this->tempFixtures.'/dir3'));\n        $this->writeRepo->add('/webmozart/dir', new DirectoryResource($this->tempFixtures.'/dir4'));\n\n        // Symlink is turned into a copy\n        $this->assertFalse(is_link($this->tempDir.'/webmozart/dir'));\n\n        // Subdirectories are merged\n        $this->assertTrue(is_dir($this->tempDir.'/webmozart/dir/sub'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/dir/sub/file1'));\n        $this->assertSame('../../../../fixtures/dir3/sub/file1', readlink($this->tempDir.'/webmozart/dir/sub/file1'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/dir/sub/file2'));\n        $this->assertSame('../../../../fixtures/dir4/sub/file2', readlink($this->tempDir.'/webmozart/dir/sub/file2'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/dir/sub/file3'));\n        $this->assertSame('../../../../fixtures/dir4/sub/file3', readlink($this->tempDir.'/webmozart/dir/sub/file3'));\n    }\n\n    public function testOverwriteDirectoryWithFileReplacesSymlink()\n    {\n        $this->writeRepo->add('/webmozart/path', new DirectoryResource($this->tempFixtures.'/dir1'));\n        $this->writeRepo->add('/webmozart/path', new FileResource($this->tempFixtures.'/dir1/file1'));\n\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/path'));\n        $this->assertSame('../../fixtures/dir1/file1', readlink($this->tempDir.'/webmozart/path'));\n    }\n\n    public function testAddFileCreatesSymlink()\n    {\n        $this->writeRepo->add('/webmozart/file', new FileResource($this->tempFixtures.'/dir1/file2'));\n\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/file'));\n        $this->assertSame('../../fixtures/dir1/file2', readlink($this->tempDir.'/webmozart/file'));\n    }\n\n    public function testOverwriteFileWithDirectoryReplacesSymlink()\n    {\n        $this->writeRepo->add('/webmozart/path', new FileResource($this->tempFixtures.'/dir1/file2'));\n        $this->writeRepo->add('/webmozart/path', new DirectoryResource($this->tempFixtures.'/dir1'));\n\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/path'));\n        $this->assertSame('../../fixtures/dir1', readlink($this->tempDir.'/webmozart/path'));\n    }\n\n    public function testOverwriteFileWithFileReplacesSymlink()\n    {\n        $this->writeRepo->add('/webmozart/path', new FileResource($this->tempFixtures.'/dir1/file2'));\n        $this->writeRepo->add('/webmozart/path', new FileResource($this->tempFixtures.'/dir1/file1'));\n\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/path'));\n        $this->assertSame('../../fixtures/dir1/file1', readlink($this->tempDir.'/webmozart/path'));\n    }\n\n    public function testAddSubDirectoryTurnsParentSymlinkIntoDirectory()\n    {\n        $this->writeRepo->add('/webmozart', new DirectoryResource($this->tempFixtures.'/dir1'));\n        $this->writeRepo->add('/webmozart/dir', new DirectoryResource($this->tempFixtures.'/dir2'));\n\n        // Symlink is turned into a copy\n        $this->assertFalse(is_link($this->tempDir.'/webmozart'));\n\n        // Directories are merged\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/file1'));\n        $this->assertSame('../../fixtures/dir1/file1', readlink($this->tempDir.'/webmozart/file1'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/file2'));\n        $this->assertSame('../../fixtures/dir1/file2', readlink($this->tempDir.'/webmozart/file2'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/dir'));\n        $this->assertSame('../../fixtures/dir2', readlink($this->tempDir.'/webmozart/dir'));\n    }\n\n    public function testAddSubFileTurnsParentSymlinkIntoDirectory()\n    {\n        $this->writeRepo->add('/webmozart', new DirectoryResource($this->tempFixtures.'/dir1'));\n        $this->writeRepo->add('/webmozart/file3', new FileResource($this->tempFixtures.'/dir2/file3'));\n\n        // Symlink is turned into a copy\n        $this->assertFalse(is_link($this->tempDir.'/webmozart'));\n\n        // Directories are merged\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/file1'));\n        $this->assertSame('../../fixtures/dir1/file1', readlink($this->tempDir.'/webmozart/file1'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/file2'));\n        $this->assertSame('../../fixtures/dir1/file2', readlink($this->tempDir.'/webmozart/file2'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/file3'));\n        $this->assertSame('../../fixtures/dir2/file3', readlink($this->tempDir.'/webmozart/file3'));\n    }\n\n    public function testAddSubResourceWithBodyTurnsParentSymlinkIntoDirectory()\n    {\n        $this->writeRepo->add('/webmozart', new DirectoryResource($this->tempFixtures.'/dir1'));\n        $this->writeRepo->add('/webmozart/file3', $this->createFile(null, 'some body'));\n\n        // Symlink is turned into a copy\n        $this->assertFalse(is_link($this->tempDir.'/webmozart'));\n\n        // Directories are merged\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/file1'));\n        $this->assertSame('../../fixtures/dir1/file1', readlink($this->tempDir.'/webmozart/file1'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/file2'));\n        $this->assertSame('../../fixtures/dir1/file2', readlink($this->tempDir.'/webmozart/file2'));\n        $this->assertFalse(is_link($this->tempDir.'/webmozart/file3'));\n        $this->assertSame('some body', file_get_contents($this->tempDir.'/webmozart/file3'));\n    }\n\n    public function testAddSubSubDirectoryTurnsParentSymlinkIntoDirectory()\n    {\n        $this->writeRepo->add('/webmozart', new DirectoryResource($this->tempFixtures.'/dir3'));\n        $this->writeRepo->add('/webmozart/sub/dir', new DirectoryResource($this->tempFixtures.'/dir1'));\n\n        // Symlink is turned into a copy\n        $this->assertFalse(is_link($this->tempDir.'/webmozart'));\n\n        // Directories are merged\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/sub/file1'));\n        $this->assertSame('../../../fixtures/dir3/sub/file1', readlink($this->tempDir.'/webmozart/sub/file1'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/sub/file2'));\n        $this->assertSame('../../../fixtures/dir3/sub/file2', readlink($this->tempDir.'/webmozart/sub/file2'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/sub/dir'));\n        $this->assertSame('../../../fixtures/dir1', readlink($this->tempDir.'/webmozart/sub/dir'));\n    }\n\n    public function testAddSubSubFileTurnsParentSymlinkIntoDirectory()\n    {\n        $this->writeRepo->add('/webmozart', new DirectoryResource($this->tempFixtures.'/dir3'));\n        $this->writeRepo->add('/webmozart/sub/file3', new FileResource($this->tempFixtures.'/dir2/file3'));\n\n        // Symlink is turned into a copy\n        $this->assertFalse(is_link($this->tempDir.'/webmozart'));\n\n        // Directories are merged\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/sub/file1'));\n        $this->assertSame('../../../fixtures/dir3/sub/file1', readlink($this->tempDir.'/webmozart/sub/file1'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/sub/file2'));\n        $this->assertSame('../../../fixtures/dir3/sub/file2', readlink($this->tempDir.'/webmozart/sub/file2'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/sub/file3'));\n        $this->assertSame('../../../fixtures/dir2/file3', readlink($this->tempDir.'/webmozart/sub/file3'));\n    }\n\n    public function testAddSubSubResourceWithBodyTurnsParentSymlinkIntoDirectory()\n    {\n        $this->writeRepo->add('/webmozart', new DirectoryResource($this->tempFixtures.'/dir3'));\n        $this->writeRepo->add('/webmozart/sub/file3', $this->createFile(null, 'some body'));\n\n        // Symlink is turned into a copy\n        $this->assertFalse(is_link($this->tempDir.'/webmozart'));\n\n        // Directories are merged\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/sub/file1'));\n        $this->assertSame('../../../fixtures/dir3/sub/file1', readlink($this->tempDir.'/webmozart/sub/file1'));\n        $this->assertTrue(is_link($this->tempDir.'/webmozart/sub/file2'));\n        $this->assertSame('../../../fixtures/dir3/sub/file2', readlink($this->tempDir.'/webmozart/sub/file2'));\n        $this->assertFalse(is_link($this->tempDir.'/webmozart/sub/file3'));\n        $this->assertSame('some body', file_get_contents($this->tempDir.'/webmozart/sub/file3'));\n    }\n}\n"
  },
  {
    "path": "tests/Fixtures/dir1/file1",
    "content": ""
  },
  {
    "path": "tests/Fixtures/dir1/file2",
    "content": ""
  },
  {
    "path": "tests/Fixtures/dir2/file2",
    "content": ""
  },
  {
    "path": "tests/Fixtures/dir2/file3",
    "content": ""
  },
  {
    "path": "tests/Fixtures/dir3/sub/file1",
    "content": ""
  },
  {
    "path": "tests/Fixtures/dir3/sub/file2",
    "content": ""
  },
  {
    "path": "tests/Fixtures/dir4/sub/file2",
    "content": ""
  },
  {
    "path": "tests/Fixtures/dir4/sub/file3",
    "content": ""
  },
  {
    "path": "tests/Fixtures/dir5/file1",
    "content": ""
  },
  {
    "path": "tests/Fixtures/dir5/file2",
    "content": ""
  },
  {
    "path": "tests/Fixtures/dir5/sub/file3",
    "content": ""
  },
  {
    "path": "tests/Fixtures/dir5/sub/file4",
    "content": ""
  },
  {
    "path": "tests/InMemoryRepositoryTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests;\n\nuse Puli\\Repository\\Api\\EditableRepository;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\nuse Puli\\Repository\\InMemoryRepository;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass InMemoryRepositoryTest extends AbstractEditableRepositoryTest\n{\n    /**\n     * @var InMemoryRepository\n     */\n    protected $repo;\n\n    protected function setUp()\n    {\n        parent::setUp();\n\n        $this->repo = new InMemoryRepository();\n    }\n\n    protected function createPrefilledRepository(PuliResource $root)\n    {\n        $repo = new InMemoryRepository();\n        $repo->add('/', $root);\n\n        return $repo;\n    }\n\n    protected function createWriteRepository()\n    {\n        return new InMemoryRepository($this->stream);\n    }\n\n    protected function createReadRepository(EditableRepository $writeRepo)\n    {\n        return $writeRepo;\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedLanguageException\n     * @expectedExceptionMessage foobar\n     */\n    public function testContainsFailsIfLanguageNotGlob()\n    {\n        $this->readRepo->contains('/*', 'foobar');\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedLanguageException\n     * @expectedExceptionMessage foobar\n     */\n    public function testFindFailsIfLanguageNotGlob()\n    {\n        $this->readRepo->find('/*', 'foobar');\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedLanguageException\n     * @expectedExceptionMessage foobar\n     */\n    public function testRemoveFailsIfLanguageNotGlob()\n    {\n        $this->writeRepo->remove('/*', 'foobar');\n    }\n}\n"
  },
  {
    "path": "tests/JsonRepositoryLoadedTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests;\n\nuse Puli\\Repository\\Api\\EditableRepository;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n * @author Titouan Galopin <galopintitouan@gmail.com>\n */\nclass JsonRepositoryLoadedTest extends JsonRepositoryTest\n{\n    protected function createReadRepository(EditableRepository $writeRepo)\n    {\n        return $writeRepo;\n    }\n}\n"
  },
  {
    "path": "tests/JsonRepositoryTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests;\n\nuse Psr\\Log\\LogLevel;\nuse Puli\\Repository\\Api\\EditableRepository;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\nuse Puli\\Repository\\JsonRepository;\nuse Puli\\Repository\\Resource\\DirectoryResource;\nuse Puli\\Repository\\Resource\\FileResource;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n * @author Titouan Galopin <galopintitouan@gmail.com>\n */\nclass JsonRepositoryTest extends AbstractJsonRepositoryTest\n{\n    protected function createPrefilledRepository(PuliResource $root)\n    {\n        $repo = new JsonRepository($this->path, $this->tempDir, true);\n        $repo->add('/', $root);\n\n        return $repo;\n    }\n\n    protected function createWriteRepository()\n    {\n        return new JsonRepository($this->path, $this->tempDir, true);\n    }\n\n    protected function createReadRepository(EditableRepository $writeRepo)\n    {\n        return new JsonRepository($this->path, $this->tempDir, true);\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testRemoveFailsWhenPassingPathsThatAreNotMappings()\n    {\n        $this->writeRepo->add('/webmozart', new DirectoryResource($this->fixtureDir.'/dir1'));\n\n        $this->writeRepo->remove('/webmozart/file1');\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\NoVersionFoundException\n     */\n    public function testGetVersionsLogsWarningIfReferenceNotFound()\n    {\n        $this->writeRepo->add('/file', new FileResource($this->fixtureDir.'/dir1/file1'));\n\n        unlink($this->fixtureDir.'/dir1/file1');\n\n        $logger = $this->getMock('Psr\\Log\\LoggerInterface');\n        $logger->expects($this->once())\n            ->method('log')\n            ->with(LogLevel::WARNING, $this->stringContains('\"fixtures/dir1/file1\"'));\n\n        $this->readRepo->setLogger($logger);\n\n        $this->readRepo->getVersions('/file');\n    }\n}\n"
  },
  {
    "path": "tests/NullRepositoryTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests;\n\nuse PHPUnit_Framework_TestCase;\nuse Puli\\Repository\\NullRepository;\nuse Puli\\Repository\\Tests\\Resource\\TestDirectory;\nuse Puli\\Repository\\Tests\\Resource\\TestFile;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass NullRepositoryTest extends PHPUnit_Framework_TestCase\n{\n    /**\n     * @var NullRepository\n     */\n    private $repo;\n\n    protected function setUp()\n    {\n        $this->repo = new NullRepository();\n    }\n\n    public function testAdd()\n    {\n        $this->repo->add('/path', new TestFile());\n\n        $this->assertFalse($this->repo->contains('/path'));\n    }\n\n    public function testRemove()\n    {\n        $this->assertFalse($this->repo->contains('/path'));\n\n        $this->repo->remove('/path');\n\n        $this->assertFalse($this->repo->contains('/path'));\n    }\n\n    public function testFind()\n    {\n        $this->repo->add('/path', new TestFile());\n\n        $this->assertCount(0, $this->repo->find('/path'));\n    }\n\n    public function testListChildren()\n    {\n        $this->repo->add('/path', new TestDirectory(null, array(\n            new TestFile('/path/file'),\n        )));\n\n        $this->assertCount(0, $this->repo->listChildren('/path'));\n        $this->assertFalse($this->repo->hasChildren('/path'));\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\ResourceNotFoundException\n     */\n    public function testGetAlwaysThrowsException()\n    {\n        $this->repo->add('/path', new TestFile());\n\n        $this->repo->get('/path');\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\NoVersionFoundException\n     */\n    public function testGetVersionsAlwaysThrowsException()\n    {\n        $this->repo->add('/path', new TestFile());\n\n        $this->repo->getVersions('/path');\n    }\n}\n"
  },
  {
    "path": "tests/OptimizedJsonRepositoryLoadedTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests;\n\nuse Puli\\Repository\\Api\\EditableRepository;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n * @author Titouan Galopin <galopintitouan@gmail.com>\n */\nclass OptimizedJsonRepositoryLoadedTest extends OptimizedJsonRepositoryTest\n{\n    protected function createReadRepository(EditableRepository $writeRepo)\n    {\n        return $writeRepo;\n    }\n}\n"
  },
  {
    "path": "tests/OptimizedJsonRepositoryTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests;\n\nuse Puli\\Repository\\Api\\EditableRepository;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\nuse Puli\\Repository\\OptimizedJsonRepository;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n * @author Titouan Galopin <galopintitouan@gmail.com>\n */\nclass OptimizedJsonRepositoryTest extends AbstractJsonRepositoryTest\n{\n    protected function createPrefilledRepository(PuliResource $root)\n    {\n        $repo = new OptimizedJsonRepository($this->path, $this->tempDir, true);\n        $repo->add('/', $root);\n\n        return $repo;\n    }\n\n    protected function createWriteRepository()\n    {\n        return new OptimizedJsonRepository($this->path, $this->tempDir, true, $this->stream);\n    }\n\n    protected function createReadRepository(EditableRepository $writeRepo)\n    {\n        return new OptimizedJsonRepository($this->path, $this->tempDir, true, $this->stream);\n    }\n}\n"
  },
  {
    "path": "tests/Resource/AbstractFilesystemResourceTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests\\Resource;\n\nuse Puli\\Repository\\Api\\Resource\\FilesystemResource;\nuse Webmozart\\PathUtil\\Path;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nabstract class AbstractFilesystemResourceTest extends AbstractResourceTest\n{\n    private $fixturesDir;\n\n    protected function setUp()\n    {\n        parent::setUp();\n\n        $this->fixturesDir = Path::normalize(__DIR__.'/Fixtures');\n    }\n\n    /**\n     * @param string|null $path\n     *\n     * @return PuliResource\n     */\n    protected function createResource($path = null)\n    {\n        return $this->createFilesystemResource($this->getValidFilesystemPath(), $path);\n    }\n\n    /**\n     * @param string      $filesystemPath\n     * @param string|null $path\n     *\n     * @return FilesystemResource\n     */\n    abstract protected function createFilesystemResource($filesystemPath, $path = null);\n\n    abstract protected function getValidFilesystemPath();\n\n    abstract protected function getValidFilesystemPath2();\n\n    abstract protected function getValidFilesystemPath3();\n\n    abstract public function getInvalidFilesystemPaths();\n\n    /**\n     * @dataProvider getInvalidFilesystemPaths\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testFailIfNonExistingFile($filesystemPath)\n    {\n        $this->createFilesystemResource($filesystemPath);\n    }\n\n    public function testGetFilesystemPath()\n    {\n        $filesystemPath = $this->getValidFilesystemPath();\n        $resource = $this->createFilesystemResource($filesystemPath);\n\n        $this->assertPathsAreEqual($filesystemPath, $resource->getFilesystemPath());\n    }\n\n    public function testAttachDoesNotChangeFilesystemPath()\n    {\n        $filesystemPath = $this->getValidFilesystemPath();\n        $resource = $this->createFilesystemResource($filesystemPath);\n        $resource->attachTo($this->repo);\n\n        $this->assertPathsAreEqual($filesystemPath, $resource->getFilesystemPath());\n    }\n\n    public function testDetachDoesNotChangeFilesystemPath()\n    {\n        $filesystemPath = $this->getValidFilesystemPath();\n        $resource = $this->createFilesystemResource($filesystemPath);\n        $resource->attachTo($this->repo);\n        $resource->detach($this->repo);\n\n        $this->assertPathsAreEqual($filesystemPath, $resource->getFilesystemPath());\n    }\n\n    public function testSerializeKeepsFilesystemPath()\n    {\n        $filesystemPath = $this->getValidFilesystemPath();\n        $resource = $this->createFilesystemResource($filesystemPath);\n\n        $deserialized = unserialize(serialize($resource));\n\n        $this->assertPathsAreEqual($filesystemPath, $deserialized->getFilesystemPath());\n    }\n\n    protected function assertPathsAreEqual($expected, $actual)\n    {\n        $normalize = function ($path) {\n            return str_replace(DIRECTORY_SEPARATOR, '/', $path);\n        };\n\n        $this->assertEquals($normalize($expected), $normalize($actual));\n    }\n}\n"
  },
  {
    "path": "tests/Resource/AbstractResourceTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests\\Resource;\n\nuse PHPUnit_Framework_MockObject_MockObject;\nuse PHPUnit_Framework_TestCase;\nuse Puli\\Repository\\Api\\ChangeStream\\VersionList;\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\nuse Puli\\Repository\\Api\\ResourceRepository;\nuse Puli\\Repository\\Resource\\Collection\\ArrayResourceCollection;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nabstract class AbstractResourceTest extends PHPUnit_Framework_TestCase\n{\n    /**\n     * @var PHPUnit_Framework_MockObject_MockObject|ResourceRepository\n     */\n    protected $repo;\n\n    /**\n     * @param string|null $path\n     *\n     * @return PuliResource\n     */\n    abstract protected function createResource($path = null);\n\n    protected function setUp()\n    {\n        $this->repo = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n    }\n\n    public function testCreate()\n    {\n        $resource = $this->createResource();\n\n        $this->assertNull($resource->getPath());\n        $this->assertNull($resource->getName());\n        $this->assertNull($resource->getRepositoryPath());\n        $this->assertNull($resource->getRepository());\n        $this->assertFalse($resource->isAttached());\n        $this->assertFalse($resource->isReference());\n    }\n\n    public function testCreateWithPath()\n    {\n        $resource = $this->createResource('/path/to/resource');\n\n        $this->assertSame('/path/to/resource', $resource->getPath());\n        $this->assertSame('resource', $resource->getName());\n        $this->assertSame('/path/to/resource', $resource->getRepositoryPath());\n        $this->assertNull($resource->getRepository());\n        $this->assertFalse($resource->isAttached());\n        $this->assertFalse($resource->isReference());\n    }\n\n    public function testAttach()\n    {\n        $resource = $this->createResource();\n        $resource->attachTo($this->repo);\n\n        $this->assertNull($resource->getPath());\n        $this->assertNull($resource->getName());\n        $this->assertNull($resource->getRepositoryPath());\n        $this->assertSame($this->repo, $resource->getRepository());\n        $this->assertTrue($resource->isAttached());\n        $this->assertFalse($resource->isReference());\n    }\n\n    public function testAttachDoesNotChangePath()\n    {\n        $resource = $this->createResource('/path/to/resource');\n        $resource->attachTo($this->repo);\n\n        $this->assertSame('/path/to/resource', $resource->getPath());\n        $this->assertSame('resource', $resource->getName());\n        $this->assertSame('/path/to/resource', $resource->getRepositoryPath());\n        $this->assertSame($this->repo, $resource->getRepository());\n        $this->assertTrue($resource->isAttached());\n        $this->assertFalse($resource->isReference());\n    }\n\n    public function testAttachSetsPathIfGiven()\n    {\n        $resource = $this->createResource('/path/to/resource');\n        $resource->attachTo($this->repo, '/path/to/attached');\n\n        $this->assertSame('/path/to/attached', $resource->getPath());\n        $this->assertSame('attached', $resource->getName());\n        $this->assertSame('/path/to/attached', $resource->getRepositoryPath());\n        $this->assertSame($this->repo, $resource->getRepository());\n        $this->assertTrue($resource->isAttached());\n        $this->assertFalse($resource->isReference());\n    }\n\n    public function testReattach()\n    {\n        $repo2 = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n\n        $resource = $this->createResource();\n        $resource->attachTo($this->repo, '/path/to/resource');\n        $resource->attachTo($repo2, '/path/to/reattached');\n\n        $this->assertSame('/path/to/reattached', $resource->getPath());\n        $this->assertSame('reattached', $resource->getName());\n        $this->assertSame('/path/to/reattached', $resource->getRepositoryPath());\n        $this->assertSame($repo2, $resource->getRepository());\n        $this->assertTrue($resource->isAttached());\n        $this->assertFalse($resource->isReference());\n    }\n\n    public function testDetach()\n    {\n        $resource = $this->createResource();\n        $resource->attachTo($this->repo);\n        $resource->detach();\n\n        $this->assertNull($resource->getPath());\n        $this->assertNull($resource->getName());\n        $this->assertNull($resource->getRepositoryPath());\n        $this->assertNull($resource->getRepository());\n        $this->assertFalse($resource->isAttached());\n        $this->assertFalse($resource->isReference());\n    }\n\n    public function testDetachKeepsPath()\n    {\n        $resource = $this->createResource();\n        $resource->attachTo($this->repo, '/path/to/resource');\n        $resource->detach();\n\n        $this->assertSame('/path/to/resource', $resource->getPath());\n        $this->assertSame('resource', $resource->getName());\n        $this->assertSame('/path/to/resource', $resource->getRepositoryPath());\n        $this->assertNull($resource->getRepository());\n        $this->assertFalse($resource->isAttached());\n        $this->assertFalse($resource->isReference());\n    }\n\n    public function testCreateReferenceToDetachedResource()\n    {\n        $resource = $this->createResource();\n\n        $reference = $resource->createReference('/path/to/reference');\n\n        $this->assertNull($resource->getPath());\n        $this->assertNull($resource->getName());\n        $this->assertNull($resource->getRepositoryPath());\n        $this->assertNull($resource->getRepository());\n        $this->assertFalse($resource->isAttached());\n        $this->assertFalse($resource->isReference());\n\n        $this->assertSame('/path/to/reference', $reference->getPath());\n        $this->assertSame('reference', $reference->getName());\n        $this->assertNull($reference->getRepositoryPath());\n        $this->assertNull($reference->getRepository());\n        $this->assertFalse($reference->isAttached());\n        $this->assertTrue($reference->isReference());\n    }\n\n    public function testCreateReferenceToDetachedResourceWithPath()\n    {\n        $resource = $this->createResource('/path/to/resource');\n\n        $reference = $resource->createReference('/path/to/reference');\n\n        $this->assertSame('/path/to/resource', $resource->getPath());\n        $this->assertSame('resource', $resource->getName());\n        $this->assertSame('/path/to/resource', $resource->getRepositoryPath());\n        $this->assertNull($resource->getRepository());\n        $this->assertFalse($resource->isAttached());\n        $this->assertFalse($resource->isReference());\n\n        $this->assertSame('/path/to/reference', $reference->getPath());\n        $this->assertSame('reference', $reference->getName());\n        $this->assertSame('/path/to/resource', $reference->getRepositoryPath());\n        $this->assertNull($reference->getRepository());\n        $this->assertFalse($reference->isAttached());\n        $this->assertTrue($reference->isReference());\n    }\n\n    public function testCreateReferenceToAttachedResource()\n    {\n        $resource = $this->createResource();\n        $resource->attachTo($this->repo, '/path/to/resource', 3);\n\n        $reference = $resource->createReference('/path/to/reference');\n\n        $this->assertSame('/path/to/resource', $resource->getPath());\n        $this->assertSame('resource', $resource->getName());\n        $this->assertSame('/path/to/resource', $resource->getRepositoryPath());\n        $this->assertSame($this->repo, $resource->getRepository());\n        $this->assertTrue($resource->isAttached());\n        $this->assertFalse($resource->isReference());\n\n        $this->assertSame('/path/to/reference', $reference->getPath());\n        $this->assertSame('reference', $reference->getName());\n        $this->assertSame('/path/to/resource', $reference->getRepositoryPath());\n        $this->assertSame($this->repo, $reference->getRepository());\n        $this->assertTrue($reference->isAttached());\n        $this->assertTrue($reference->isReference());\n    }\n\n    public function testAttachDetachedReference()\n    {\n        $resource = $this->createResource();\n\n        $reference = $resource->createReference('/path/to/reference', 3);\n        $detached = clone $reference;\n        $reference->attachTo($this->repo);\n        $attached = clone $reference;\n\n        $this->assertSame('/path/to/reference', $reference->getPath());\n        $this->assertSame('reference', $reference->getName());\n        $this->assertNull($reference->getRepositoryPath());\n        $this->assertSame($this->repo, $reference->getRepository());\n        $this->assertTrue($reference->isAttached());\n        $this->assertTrue($reference->isReference());\n\n        $reference->detach();\n\n        $this->assertEquals($detached, $reference);\n\n        $reference->attachTo($this->repo);\n\n        $this->assertEquals($attached, $reference);\n    }\n\n    public function testAttachDetachedReferenceWithPath()\n    {\n        $resource = $this->createResource();\n\n        $reference = $resource->createReference('/path/to/reference');\n        $reference->attachTo($this->repo, '/path/to/attached');\n\n        // References are dereferenced when a path is passed to attachTo()\n        $this->assertSame('/path/to/attached', $reference->getPath());\n        $this->assertSame('attached', $reference->getName());\n        $this->assertSame('/path/to/attached', $reference->getRepositoryPath());\n        $this->assertSame($this->repo, $reference->getRepository());\n        $this->assertTrue($reference->isAttached());\n        $this->assertFalse($reference->isReference());\n    }\n\n    public function testReattachAttachedReference()\n    {\n        $resource = $this->createResource();\n        $resource->attachTo($this->repo, '/path/to/resource');\n\n        $reference = $resource->createReference('/path/to/reference');\n        $detached = clone $reference;\n        $detached->detach();\n        $reference->attachTo($this->repo);\n        $attached = clone $reference;\n\n        $this->assertSame('/path/to/reference', $reference->getPath());\n        $this->assertSame('reference', $reference->getName());\n        $this->assertSame('/path/to/resource', $reference->getRepositoryPath());\n        $this->assertSame($this->repo, $reference->getRepository());\n        $this->assertTrue($reference->isAttached());\n        $this->assertTrue($reference->isReference());\n\n        $reference->detach();\n\n        $this->assertEquals($detached, $reference);\n\n        $reference->attachTo($this->repo);\n\n        $this->assertEquals($attached, $reference);\n    }\n\n    public function testReattachAttachedReferenceWithPath()\n    {\n        $resource = $this->createResource();\n        $resource->attachTo($this->repo, '/path/to/resource');\n\n        $reference = $resource->createReference('/path/to/reference');\n        $reference->attachTo($this->repo, '/path/to/attached');\n\n        // References are dereferenced when a path is passed to attachTo()\n        $this->assertSame('/path/to/attached', $reference->getPath());\n        $this->assertSame('attached', $reference->getName());\n        $this->assertSame('/path/to/attached', $reference->getRepositoryPath());\n        $this->assertSame($this->repo, $reference->getRepository());\n        $this->assertTrue($reference->isAttached());\n        $this->assertFalse($reference->isReference());\n    }\n\n    public function testSerializeDetachedResource()\n    {\n        $resource = $this->createResource();\n\n        $deserialized = unserialize(serialize($resource));\n\n        $this->assertNull($deserialized->getPath());\n        $this->assertNull($deserialized->getName());\n        $this->assertNull($deserialized->getRepositoryPath());\n        $this->assertNull($deserialized->getRepository());\n        $this->assertFalse($deserialized->isAttached());\n        $this->assertFalse($deserialized->isReference());\n    }\n\n    public function testSerializeDetachedResourceWithPath()\n    {\n        $resource = $this->createResource('/path/to/resource', 3);\n\n        $deserialized = unserialize(serialize($resource));\n\n        $this->assertSame('/path/to/resource', $deserialized->getPath());\n        $this->assertSame('resource', $deserialized->getName());\n        $this->assertSame('/path/to/resource', $deserialized->getRepositoryPath());\n        $this->assertNull($deserialized->getRepository());\n        $this->assertFalse($deserialized->isAttached());\n        $this->assertFalse($deserialized->isReference());\n    }\n\n    public function testSerializeAttachedResourceDetachesResource()\n    {\n        $resource = $this->createResource();\n        $resource->attachTo($this->repo, '/path/to/resource', 3);\n\n        $deserialized = unserialize(serialize($resource));\n\n        $this->assertSame('/path/to/resource', $deserialized->getPath());\n        $this->assertSame('resource', $deserialized->getName());\n        $this->assertSame('/path/to/resource', $deserialized->getRepositoryPath());\n        $this->assertNull($deserialized->getRepository());\n        $this->assertFalse($deserialized->isAttached());\n        $this->assertFalse($deserialized->isReference());\n    }\n\n    public function testSerializeDetachedReference()\n    {\n        $resource = $this->createResource();\n        $reference = $resource->createReference('/path/to/reference');\n\n        $deserialized = unserialize(serialize($reference));\n\n        $this->assertSame('/path/to/reference', $deserialized->getPath());\n        $this->assertSame('reference', $deserialized->getName());\n        $this->assertNull($deserialized->getRepositoryPath());\n        $this->assertNull($deserialized->getRepository());\n        $this->assertFalse($deserialized->isAttached());\n        $this->assertTrue($deserialized->isReference());\n    }\n\n    public function testSerializeDetachedReferenceWithPath()\n    {\n        $resource = $this->createResource('/path/to/resource', 3);\n        $reference = $resource->createReference('/path/to/reference');\n\n        $deserialized = unserialize(serialize($reference));\n\n        $this->assertSame('/path/to/reference', $deserialized->getPath());\n        $this->assertSame('reference', $deserialized->getName());\n        $this->assertSame('/path/to/resource', $deserialized->getRepositoryPath());\n        $this->assertNull($deserialized->getRepository());\n        $this->assertFalse($deserialized->isAttached());\n        $this->assertTrue($deserialized->isReference());\n    }\n\n    public function testSerializeAttachedReferenceDetachesReference()\n    {\n        $resource = $this->createResource();\n        $resource->attachTo($this->repo, '/path/to/resource', 3);\n        $reference = $resource->createReference('/path/to/reference');\n\n        $deserialized = unserialize(serialize($reference));\n\n        $this->assertSame('/path/to/reference', $deserialized->getPath());\n        $this->assertSame('reference', $deserialized->getName());\n        $this->assertSame('/path/to/resource', $deserialized->getRepositoryPath());\n        $this->assertNull($deserialized->getRepository());\n        $this->assertFalse($deserialized->isAttached());\n        $this->assertTrue($deserialized->isReference());\n    }\n\n    public function testListChildren()\n    {\n        $file1 = new TestFile('/file1');\n        $file2 = new TestFile('/file2');\n        $resources = new ArrayResourceCollection(array($file1, $file2));\n        $repo = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n\n        $repo->expects($this->once())\n            ->method('listChildren')\n            ->with('/path')\n            ->will($this->returnValue($resources));\n\n        $resource = $this->createResource('/path');\n        $resource->attachTo($repo);\n\n        $children = $resource->listChildren();\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\ResourceCollection', $children);\n        $this->assertEquals(array('file1' => $file1, 'file2' => $file2), $children->toArray());\n    }\n\n    public function testListChildrenWithReference()\n    {\n        $file1 = new TestFile('/file1');\n        $file2 = new TestFile('/file2');\n        $resources = new ArrayResourceCollection(array($file1, $file2));\n        $repo = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n\n        $repo->expects($this->once())\n            ->method('listChildren')\n            // use the repository path, not the reference path\n            ->with('/path')\n            ->will($this->returnValue($resources));\n\n        $resource = $this->createResource('/path');\n        $resource->attachTo($repo);\n\n        $reference = $resource->createReference('/reference');\n\n        $children = $reference->listChildren();\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\ResourceCollection', $children);\n        $this->assertEquals(array('file1' => $file1, 'file2' => $file2), $children->toArray());\n    }\n\n    public function testListChildrenDetached()\n    {\n        $resource = $this->createResource();\n\n        $children = $resource->listChildren();\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\ResourceCollection', $children);\n        $this->assertEquals(array(), $children->toArray());\n    }\n\n    public function testGetChild()\n    {\n        $child = $this->getMock('Puli\\Repository\\Api\\Resource\\PuliResource');\n        $repo = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n\n        $repo->expects($this->once())\n            ->method('get')\n            ->with('/path/file')\n            ->will($this->returnValue($child));\n\n        $resource = $this->createResource('/path');\n        $resource->attachTo($repo);\n\n        $this->assertSame($child, $resource->getChild('file'));\n    }\n\n    public function testGetChildWithReference()\n    {\n        $child = $this->getMock('Puli\\Repository\\Api\\Resource\\PuliResource');\n        $repo = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n\n        $repo->expects($this->once())\n            ->method('get')\n            // use the repository path, not the reference path\n            ->with('/path/file')\n            ->will($this->returnValue($child));\n\n        $resource = $this->createResource('/path');\n        $resource->attachTo($repo);\n\n        $reference = $resource->createReference('/reference');\n\n        $this->assertSame($child, $reference->getChild('file'));\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\ResourceNotFoundException\n     */\n    public function testGetChildDetached()\n    {\n        $resource = $this->createResource();\n\n        $resource->getChild('file');\n    }\n\n    public function testHasChild()\n    {\n        $repo = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n\n        $repo->expects($this->once())\n            ->method('contains')\n            ->with('/path/file')\n            ->will($this->returnValue('true_or_false'));\n\n        $resource = $this->createResource('/path');\n        $resource->attachTo($repo);\n\n        $this->assertSame('true_or_false', $resource->hasChild('file'));\n    }\n\n    public function testHasChildWithReference()\n    {\n        $repo = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n\n        $repo->expects($this->once())\n            ->method('contains')\n            // use the repository path, not the reference path\n            ->with('/path/file')\n            ->will($this->returnValue('true_or_false'));\n\n        $resource = $this->createResource('/path');\n        $resource->attachTo($repo);\n\n        $reference = $resource->createReference('/reference');\n\n        $this->assertSame('true_or_false', $reference->hasChild('file'));\n    }\n\n    public function testHasChildDetached()\n    {\n        $resource = $this->createResource();\n\n        $this->assertFalse($resource->hasChild('file'));\n    }\n\n    public function testHasChildren()\n    {\n        $repo = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n\n        $repo->expects($this->once())\n            ->method('hasChildren')\n            ->with('/path')\n            ->will($this->returnValue('true_or_false'));\n\n        $resource = $this->createResource('/path');\n        $resource->attachTo($repo);\n\n        $this->assertSame('true_or_false', $resource->hasChildren());\n    }\n\n    public function testHasChildrenWithReference()\n    {\n        $repo = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n\n        $repo->expects($this->once())\n            ->method('hasChildren')\n            // use the repository path, not the reference path\n            ->with('/path')\n            ->will($this->returnValue('true_or_false'));\n\n        $resource = $this->createResource('/path');\n        $resource->attachTo($repo);\n\n        $reference = $resource->createReference('/reference');\n\n        $this->assertSame('true_or_false', $reference->hasChildren());\n    }\n\n    public function testHasChildrenDetached()\n    {\n        $resource = $this->createResource();\n\n        $this->assertFalse($resource->hasChildren());\n    }\n\n    public function testGetVersions()\n    {\n        $repo = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n\n        $resource = $this->createResource('/path');\n        $resource->attachTo($repo);\n\n        $versions = new VersionList('/path', array($resource));\n\n        $repo->expects($this->once())\n            ->method('getVersions')\n            // use the repository path, not the reference path\n            ->with('/path')\n            ->will($this->returnValue($versions));\n\n        $this->assertSame($versions, $resource->getVersions());\n    }\n\n    public function testGetVersionsDetached()\n    {\n        $resource = $this->createResource('/path');\n\n        $versions = new VersionList('/path', array($resource));\n\n        $this->assertEquals($versions, $resource->getVersions());\n    }\n}\n"
  },
  {
    "path": "tests/Resource/Collection/ArrayResourceCollectionTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests\\Resource\\Collection;\n\nuse PHPUnit_Framework_TestCase;\nuse Puli\\Repository\\Resource\\Collection\\ArrayResourceCollection;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass ArrayResourceCollectionTest extends PHPUnit_Framework_TestCase\n{\n    public function testConstruct()\n    {\n        $collection = new ArrayResourceCollection(array(\n            $dir = $this->getMock('Puli\\Repository\\Api\\Resource\\PuliResource'),\n            $file = $this->getMock('Puli\\Repository\\Api\\Resource\\BodyResource'),\n        ));\n\n        $this->assertCount(2, $collection);\n        $this->assertSame(array($dir, $file), $collection->toArray());\n        $this->assertSame($dir, $collection->get(0));\n        $this->assertSame($file, $collection->get(1));\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testConstructFailsIfNoTraversable()\n    {\n        new ArrayResourceCollection('foobar');\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testConstructFailsIfNoResource()\n    {\n        new ArrayResourceCollection(array(\n            'foobar',\n        ));\n    }\n\n    public function testReplace()\n    {\n        $collection = new ArrayResourceCollection(array(\n            $this->getMock('Puli\\Repository\\Api\\Resource\\PuliResource'),\n            $this->getMock('Puli\\Repository\\Api\\Resource\\PuliResource'),\n        ));\n\n        $collection->replace(array(\n            2 => $dir = $this->getMock('Puli\\Repository\\Api\\Resource\\PuliResource'),\n            3 => $file = $this->getMock('Puli\\Repository\\Api\\Resource\\BodyResource'),\n        ));\n\n        $this->assertCount(2, $collection);\n        $this->assertSame(array(2 => $dir, 3 => $file), $collection->toArray());\n        $this->assertSame($dir, $collection->get(2));\n        $this->assertSame($file, $collection->get(3));\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testReplaceFailsIfNoTraversable()\n    {\n        $collection = new ArrayResourceCollection();\n\n        $collection->replace('foobar');\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testReplaceFailsIfNoResource()\n    {\n        $collection = new ArrayResourceCollection();\n\n        $collection->replace(array(\n            'foobar',\n        ));\n    }\n\n    public function testMerge()\n    {\n        $collection = new ArrayResourceCollection(array(\n            2 => $dir1 = $this->getMock('Puli\\Repository\\Api\\Resource\\PuliResource'),\n            3 => $dir2 = $this->getMock('Puli\\Repository\\Api\\Resource\\PuliResource'),\n        ));\n\n        $collection->merge(array(\n            $dir3 = $this->getMock('Puli\\Repository\\Api\\Resource\\PuliResource'),\n            $file = $this->getMock('Puli\\Repository\\Api\\Resource\\BodyResource'),\n        ));\n\n        $this->assertCount(4, $collection);\n        $this->assertSame(array(2 => $dir1, 3 => $dir2, 4 => $dir3, 5 => $file), $collection->toArray());\n        $this->assertSame($dir1, $collection->get(2));\n        $this->assertSame($dir2, $collection->get(3));\n        $this->assertSame($dir3, $collection->get(4));\n        $this->assertSame($file, $collection->get(5));\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testMergeFailsIfNoTraversable()\n    {\n        $collection = new ArrayResourceCollection();\n\n        $collection->merge('foobar');\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testMergeFailsIfNoResource()\n    {\n        $collection = new ArrayResourceCollection();\n\n        $collection->merge(array(\n            'foobar',\n        ));\n    }\n\n    /**\n     * @expectedException \\OutOfBoundsException\n     */\n    public function testGetFailsIfNoSuchOffset()\n    {\n        $collection = new ArrayResourceCollection();\n\n        $collection->get(0);\n    }\n\n    public function testSet()\n    {\n        $collection = new ArrayResourceCollection(array(\n            1 => $this->getMock('Puli\\Repository\\Api\\Resource\\PuliResource'),\n            2 => $file1 = $this->getMock('Puli\\Repository\\Api\\Resource\\BodyResource'),\n        ));\n\n        $this->assertCount(2, $collection);\n        $this->assertSame($file1, $collection->get(2));\n\n        $collection->set(2, $file2 = $this->getMock('Puli\\Repository\\Api\\Resource\\BodyResource'));\n\n        $this->assertCount(2, $collection);\n        $this->assertSame($file2, $collection->get(2));\n    }\n\n    public function testRemove()\n    {\n        $collection = new ArrayResourceCollection(array(\n            $dir1 = $this->getMock('Puli\\Repository\\Api\\Resource\\PuliResource'),\n            $dir2 = $this->getMock('Puli\\Repository\\Api\\Resource\\PuliResource'),\n            $file = $this->getMock('Puli\\Repository\\Api\\Resource\\BodyResource'),\n        ));\n\n        $collection->remove(1);\n\n        $this->assertCount(2, $collection);\n        $this->assertSame(array($dir1, 2 => $file), $collection->toArray());\n        $this->assertSame($dir1, $collection->get(0));\n        $this->assertSame($file, $collection->get(2));\n    }\n\n    public function testHas()\n    {\n        $collection = new ArrayResourceCollection(array(\n            $dir1 = $this->getMock('Puli\\Repository\\Api\\Resource\\PuliResource'),\n            $dir2 = $this->getMock('Puli\\Repository\\Api\\Resource\\PuliResource'),\n            $file = $this->getMock('Puli\\Repository\\Api\\Resource\\BodyResource'),\n        ));\n\n        $this->assertFalse($collection->has(-1));\n        $this->assertTrue($collection->has(0));\n        $this->assertTrue($collection->has(1));\n        $this->assertTrue($collection->has(2));\n        $this->assertFalse($collection->has(3));\n    }\n\n    public function testClear()\n    {\n        $collection = new ArrayResourceCollection(array(\n            $dir1 = $this->getMock('Puli\\Repository\\Api\\Resource\\PuliResource'),\n            $dir2 = $this->getMock('Puli\\Repository\\Api\\Resource\\PuliResource'),\n            $file = $this->getMock('Puli\\Repository\\Api\\Resource\\BodyResource'),\n        ));\n\n        $collection->clear();\n\n        $this->assertCount(0, $collection);\n    }\n\n    public function testAdd()\n    {\n        $collection = new ArrayResourceCollection(array(\n            $dir = $this->getMock('Puli\\Repository\\Api\\Resource\\PuliResource'),\n        ));\n\n        $collection->add($file = $this->getMock('Puli\\Repository\\Api\\Resource\\BodyResource'));\n\n        $this->assertCount(2, $collection);\n        $this->assertSame(array($dir, $file), $collection->toArray());\n        $this->assertSame($dir, $collection->get(0));\n        $this->assertSame($file, $collection->get(1));\n    }\n\n    public function testIsEmpty()\n    {\n        $collection = new ArrayResourceCollection();\n\n        $this->assertTrue($collection->isEmpty());\n\n        $collection->add($this->getMock('Puli\\Repository\\Api\\Resource\\BodyResource'));\n\n        $this->assertFalse($collection->isEmpty());\n\n        $collection->remove(0);\n\n        $this->assertTrue($collection->isEmpty());\n    }\n\n    public function testArrayAccess()\n    {\n        $collection = new ArrayResourceCollection();\n        $collection[] = $this->getMock('Puli\\Repository\\Api\\Resource\\BodyResource');\n    }\n}\n"
  },
  {
    "path": "tests/Resource/Collection/FilesystemResourceCollectionTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests\\Resource\\Collection;\n\nuse PHPUnit_Framework_TestCase;\nuse Puli\\Repository\\Resource\\Collection\\FilesystemResourceCollection;\nuse Puli\\Repository\\Resource\\DirectoryResource;\nuse Puli\\Repository\\Resource\\FileResource;\nuse Puli\\Repository\\Tests\\Resource\\TestDirectory;\nuse Puli\\Repository\\Tests\\Resource\\TestFile;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass FilesystemResourceCollectionTest extends PHPUnit_Framework_TestCase\n{\n    private $fixturesDir;\n\n    protected function setUp()\n    {\n        $this->fixturesDir = __DIR__.'/../Fixtures';\n    }\n\n    public function testConstruct()\n    {\n        $collection = new FilesystemResourceCollection(array(\n            $dir = new DirectoryResource($this->fixturesDir.'/dir1'),\n            $file = new FileResource($this->fixturesDir.'/file3'),\n        ));\n\n        $this->assertCount(2, $collection);\n        $this->assertSame(array($dir, $file), $collection->toArray());\n        $this->assertSame($dir, $collection->get(0));\n        $this->assertSame($file, $collection->get(1));\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testConstructFailsIfNoTraversable()\n    {\n        new FilesystemResourceCollection('foobar');\n    }\n\n    public function testReplace()\n    {\n        $collection = new FilesystemResourceCollection(array(\n            new DirectoryResource($this->fixturesDir.'/dir1'),\n        ));\n\n        $collection->replace(array(\n            $dir = new DirectoryResource($this->fixturesDir.'/dir2'),\n            $file = new FileResource($this->fixturesDir.'/file3'),\n        ));\n\n        $this->assertCount(2, $collection);\n        $this->assertSame(array($dir, $file), $collection->toArray());\n        $this->assertSame($dir, $collection->get(0));\n        $this->assertSame($file, $collection->get(1));\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testReplaceFailsIfNoTraversable()\n    {\n        $collection = new FilesystemResourceCollection();\n\n        $collection->replace('foobar');\n    }\n\n    public function testAdd()\n    {\n        $collection = new FilesystemResourceCollection(array(\n            $dir = new DirectoryResource($this->fixturesDir.'/dir1'),\n        ));\n\n        $collection->add($file = new FileResource($this->fixturesDir.'/file3'));\n\n        $this->assertCount(2, $collection);\n        $this->assertSame(array($dir, $file), $collection->toArray());\n        $this->assertSame($dir, $collection->get(0));\n        $this->assertSame($file, $collection->get(1));\n    }\n\n    public function testGetFilesystemPaths()\n    {\n        $collection = new FilesystemResourceCollection(array(\n            $dir = new DirectoryResource($this->fixturesDir.'/dir1'),\n            $file = new FileResource($this->fixturesDir.'/file3'),\n        ));\n\n        $this->assertSame(array(\n            $dir->getFilesystemPath(),\n            $file->getFilesystemPath(),\n        ), $collection->getFilesystemPaths());\n    }\n\n    public function testGetFilesystemPathsIgnoresNonFilesystemResources()\n    {\n        $collection = new FilesystemResourceCollection(array(\n            $dir = new DirectoryResource($this->fixturesDir.'/dir1'),\n            $file = new FileResource($this->fixturesDir.'/file3'),\n            new TestFile(),\n            new TestDirectory(),\n        ));\n\n        $this->assertSame(array(\n            $dir->getFilesystemPath(),\n            $file->getFilesystemPath(),\n        ), $collection->getFilesystemPaths());\n    }\n\n    public function testGetFilesystemPathsIgnoresResourcesWithEmptyFilesystemPaths()\n    {\n        $collection = new FilesystemResourceCollection(array(\n            $dir = new DirectoryResource($this->fixturesDir.'/dir1'),\n            $file = new FileResource($this->fixturesDir.'/file3'),\n            $this->getMock('Puli\\Repository\\Api\\Resource\\FilesystemResource'),\n        ));\n\n        $this->assertSame(array(\n            $dir->getFilesystemPath(),\n            $file->getFilesystemPath(),\n        ), $collection->getFilesystemPaths());\n    }\n}\n"
  },
  {
    "path": "tests/Resource/DirectoryResourceTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests\\Resource;\n\nuse Puli\\Repository\\Resource\\DirectoryResource;\nuse Puli\\Repository\\Resource\\FileResource;\nuse Symfony\\Component\\Filesystem\\Filesystem;\nuse Webmozart\\Glob\\Test\\TestUtil;\nuse Webmozart\\PathUtil\\Path;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass DirectoryResourceTest extends AbstractFilesystemResourceTest\n{\n    private $fixturesDir;\n\n    private $tempEmptyDir;\n\n    protected function setUp()\n    {\n        parent::setUp();\n\n        $this->tempEmptyDir = TestUtil::makeTempDir('puli-repository', __CLASS__);\n        $this->fixturesDir = Path::normalize(realpath(__DIR__.'/Fixtures'));\n    }\n\n    protected function tearDown()\n    {\n        $filesystem = new Filesystem();\n        $filesystem->remove($this->tempEmptyDir);\n\n        parent::tearDown();\n    }\n\n    protected function createFilesystemResource($filesystemPath, $path = null)\n    {\n        return new DirectoryResource($filesystemPath, $path);\n    }\n\n    protected function getValidFilesystemPath()\n    {\n        return $this->fixturesDir.'/dir1';\n    }\n\n    protected function getValidFilesystemPath2()\n    {\n        return $this->fixturesDir.'/dir2';\n    }\n\n    protected function getValidFilesystemPath3()\n    {\n        return $this->fixturesDir.'/empty';\n    }\n\n    public function getInvalidFilesystemPaths()\n    {\n        // setUp() has not yet been called in the data provider\n        $fixturesDir = Path::normalize(realpath(__DIR__.'/Fixtures'));\n\n        return array(\n            // No directory\n            array($fixturesDir.'/file3'),\n            // Does not exist\n            array($fixturesDir.'/foobar'),\n        );\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testFailIfNoDirectory()\n    {\n        new DirectoryResource($this->fixturesDir.'/dir1/file1');\n    }\n\n    public function testListChildrenDetached()\n    {\n        $resource = new DirectoryResource($this->fixturesDir.'/dir1');\n\n        $children = $resource->listChildren();\n\n        $this->assertCount(2, $children);\n        $this->assertInstanceOf('Puli\\Repository\\Resource\\Collection\\FilesystemResourceCollection', $children);\n        $this->assertEquals(new FileResource($this->fixturesDir.'/dir1/file1'), $children['file1']);\n        $this->assertEquals(new FileResource($this->fixturesDir.'/dir1/file2'), $children['file2']);\n    }\n\n    public function testGetChildDetached()\n    {\n        $resource = new DirectoryResource($this->fixturesDir.'/dir1');\n\n        $this->assertEquals(new FileResource($this->fixturesDir.'/dir1/file1'), $resource->getChild('file1'));\n    }\n\n    public function testHasChildDetached()\n    {\n        $resource = new DirectoryResource($this->fixturesDir.'/dir1');\n\n        $this->assertTrue($resource->hasChild('file1'));\n        $this->assertTrue($resource->hasChild('file2'));\n        $this->assertTrue($resource->hasChild('.'));\n        $this->assertTrue($resource->hasChild('..'));\n        $this->assertFalse($resource->hasChild('foobar'));\n    }\n\n    public function testHasChildrenDetached()\n    {\n        $resource = new DirectoryResource($this->fixturesDir.'/dir1');\n\n        $this->assertTrue($resource->hasChildren());\n\n        $resource = new DirectoryResource($this->tempEmptyDir);\n\n        $this->assertFalse($resource->hasChildren());\n    }\n}\n"
  },
  {
    "path": "tests/Resource/FileResourceTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests\\Resource;\n\nuse Puli\\Repository\\Resource\\FileResource;\nuse Webmozart\\PathUtil\\Path;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass FileResourceTest extends AbstractFilesystemResourceTest\n{\n    private $fixturesDir;\n\n    protected function setUp()\n    {\n        parent::setUp();\n\n        $this->fixturesDir = Path::normalize(realpath(__DIR__.'/Fixtures'));\n    }\n\n    protected function createFilesystemResource($resourcesystemPath, $path = null)\n    {\n        return new FileResource($resourcesystemPath, $path);\n    }\n\n    protected function getValidFilesystemPath()\n    {\n        return $this->fixturesDir.'/dir1/file1';\n    }\n\n    protected function getValidFilesystemPath2()\n    {\n        return $this->fixturesDir.'/dir1/file2';\n    }\n\n    protected function getValidFilesystemPath3()\n    {\n        return $this->fixturesDir.'/dir2/file1';\n    }\n\n    public function getInvalidFilesystemPaths()\n    {\n        // setUp() has not yet been called in the data provider\n        $fixturesDir = Path::normalize(realpath(__DIR__.'/Fixtures'));\n\n        return array(\n            // Not a file\n            array($fixturesDir.'/dir1'),\n            // Does not exist\n            array($fixturesDir.'/foobar'),\n        );\n    }\n\n    public function testGetContents()\n    {\n        $resource = new FileResource($this->fixturesDir.'/dir1/file1');\n\n        $this->assertSame(file_get_contents($resource->getFilesystemPath()), $resource->getBody());\n    }\n\n    public function testListChildren()\n    {\n        $repo = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n\n        $repo->expects($this->never())\n            ->method('listChildren');\n\n        $resource = new FileResource($this->fixturesDir.'/dir1/file1');\n        $resource->attachTo($repo);\n\n        $children = $resource->listChildren();\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\ResourceCollection', $children);\n        $this->assertEquals(array(), $children->toArray());\n    }\n\n    public function testListChildrenWithReference()\n    {\n        $repo = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n\n        $repo->expects($this->never())\n            ->method('listChildren');\n\n        $resource = new FileResource($this->fixturesDir.'/dir1/file1');\n        $resource->attachTo($repo);\n\n        $reference = $resource->createReference('/reference');\n\n        $children = $reference->listChildren();\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\ResourceCollection', $children);\n        $this->assertEquals(array(), $children->toArray());\n    }\n\n    public function testListChildrenDetached()\n    {\n        $resource = new FileResource($this->fixturesDir.'/dir1/file1');\n\n        $children = $resource->listChildren();\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\ResourceCollection', $children);\n        $this->assertEquals(array(), $children->toArray());\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\ResourceNotFoundException\n     */\n    public function testGetChild()\n    {\n        $repo = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n\n        $repo->expects($this->never())\n            ->method('get');\n\n        $resource = new FileResource($this->fixturesDir.'/dir1/file1');\n        $resource->attachTo($repo);\n\n        $resource->getChild('file');\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\ResourceNotFoundException\n     */\n    public function testGetChildWithReference()\n    {\n        $repo = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n\n        $repo->expects($this->never())\n            ->method('get');\n\n        $resource = new FileResource($this->fixturesDir.'/dir1/file1');\n        $resource->attachTo($repo);\n\n        $reference = $resource->createReference('/reference');\n\n        $reference->getChild('file');\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\ResourceNotFoundException\n     */\n    public function testGetChildDetached()\n    {\n        $resource = new FileResource($this->fixturesDir.'/dir1/file1');\n\n        $resource->getChild('file');\n    }\n\n    public function testHasChild()\n    {\n        $repo = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n\n        $repo->expects($this->never())\n            ->method('contains');\n\n        $resource = new FileResource($this->fixturesDir.'/dir1/file1');\n        $resource->attachTo($repo);\n\n        $this->assertFalse($resource->hasChild('file'));\n    }\n\n    public function testHasChildWithReference()\n    {\n        $repo = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n\n        $repo->expects($this->never())\n            ->method('contains');\n\n        $resource = new FileResource($this->fixturesDir.'/dir1/file1');\n        $resource->attachTo($repo);\n\n        $reference = $resource->createReference('/reference');\n\n        $this->assertFalse($reference->hasChild('file'));\n    }\n\n    public function testHasChildDetached()\n    {\n        $resource = new FileResource($this->fixturesDir.'/dir1/file1');\n\n        $this->assertFalse($resource->hasChild('file'));\n    }\n\n    public function testHasChildren()\n    {\n        $repo = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n\n        $repo->expects($this->never())\n            ->method('hasChildren');\n\n        $resource = new FileResource($this->fixturesDir.'/dir1/file1');\n        $resource->attachTo($repo);\n\n        $this->assertFalse($resource->hasChildren());\n    }\n\n    public function testHasChildrenWithReference()\n    {\n        $repo = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n\n        $repo->expects($this->never())\n            ->method('hasChildren');\n\n        $resource = new FileResource($this->fixturesDir.'/dir1/file1');\n        $resource->attachTo($repo);\n\n        $reference = $resource->createReference('/reference');\n\n        $this->assertFalse($reference->hasChildren());\n    }\n\n    public function testHasChildrenDetached()\n    {\n        $resource = new FileResource($this->fixturesDir.'/dir1/file1');\n\n        $this->assertFalse($resource->hasChildren());\n    }\n}\n"
  },
  {
    "path": "tests/Resource/Fixtures/dir1/file1",
    "content": "LINE 1\nLINE 2\n"
  },
  {
    "path": "tests/Resource/Fixtures/dir1/file2",
    "content": "NEW DATANEW\n"
  },
  {
    "path": "tests/Resource/Fixtures/dir2/.dotfile",
    "content": ""
  },
  {
    "path": "tests/Resource/Fixtures/dir2/file1",
    "content": ""
  },
  {
    "path": "tests/Resource/Fixtures/file3",
    "content": ""
  },
  {
    "path": "tests/Resource/GenericResourceTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests\\Resource;\n\nuse Puli\\Repository\\Resource\\GenericResource;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass GenericResourceTest extends AbstractResourceTest\n{\n    protected function createResource($path = null)\n    {\n        return new GenericResource($path);\n    }\n}\n"
  },
  {
    "path": "tests/Resource/Iterator/ResourceCollectionIteratorTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests\\Resource\\Iterator;\n\nuse PHPUnit_Framework_TestCase;\nuse Puli\\Repository\\Resource\\Collection\\ArrayResourceCollection;\nuse Puli\\Repository\\Resource\\Iterator\\RecursiveResourceIteratorIterator;\nuse Puli\\Repository\\Resource\\Iterator\\ResourceCollectionIterator;\nuse Puli\\Repository\\Tests\\Resource\\TestDirectory;\nuse Puli\\Repository\\Tests\\Resource\\TestFile;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass ResourceCollectionIteratorTest extends PHPUnit_Framework_TestCase\n{\n    public function testDefaultIteration()\n    {\n        $collection = new ArrayResourceCollection(array(\n            $dir1 = new TestDirectory('/webmozart', array(\n                $dir11 = new TestDirectory('/webmozart/puli', array(\n                    $dir111 = new TestDirectory('/webmozart/puli/config', array(\n                        $file1111 = new TestFile('/webmozart/puli/config/config.yml'),\n                        $file1112 = new TestFile('/webmozart/puli/config/routing.yml'),\n                    )),\n                    $dir112 = new TestDirectory('/webmozart/puli/css', array(\n                        $file1121 = new TestFile('/webmozart/puli/css/style.css'),\n                    )),\n                    $file113 = new TestFile('/webmozart/puli/installer.json'),\n                )),\n            )),\n        ));\n\n        $iterator = new RecursiveResourceIteratorIterator(\n            new ResourceCollectionIterator($collection),\n            RecursiveResourceIteratorIterator::SELF_FIRST\n        );\n\n        $expected = array(\n            '/webmozart' => $dir1,\n            '/webmozart/puli' => $dir11,\n            '/webmozart/puli/config' => $dir111,\n            '/webmozart/puli/config/config.yml' => $file1111,\n            '/webmozart/puli/config/routing.yml' => $file1112,\n            '/webmozart/puli/css' => $dir112,\n            '/webmozart/puli/css/style.css' => $file1121,\n            '/webmozart/puli/installer.json' => $file113,\n        );\n\n        $this->assertSame($expected, iterator_to_array($iterator));\n    }\n\n    public function testCurrentAsResource()\n    {\n        $collection = new ArrayResourceCollection(array(\n            $dir1 = new TestDirectory('/webmozart', array(\n                $dir11 = new TestDirectory('/webmozart/puli', array(\n                    $dir111 = new TestDirectory('/webmozart/puli/config', array(\n                        $file1111 = new TestFile('/webmozart/puli/config/config.yml'),\n                        $file1112 = new TestFile('/webmozart/puli/config/routing.yml'),\n                    )),\n                    $dir112 = new TestDirectory('/webmozart/puli/css', array(\n                        $file1121 = new TestFile('/webmozart/puli/css/style.css'),\n                    )),\n                    $file113 = new TestFile('/webmozart/puli/installer.json'),\n                )),\n            )),\n        ));\n\n        $iterator = new RecursiveResourceIteratorIterator(\n            new ResourceCollectionIterator(\n                $collection,\n                ResourceCollectionIterator::CURRENT_AS_RESOURCE\n            ),\n            RecursiveResourceIteratorIterator::SELF_FIRST\n        );\n\n        $expected = array(\n            '/webmozart' => $dir1,\n            '/webmozart/puli' => $dir11,\n            '/webmozart/puli/config' => $dir111,\n            '/webmozart/puli/config/config.yml' => $file1111,\n            '/webmozart/puli/config/routing.yml' => $file1112,\n            '/webmozart/puli/css' => $dir112,\n            '/webmozart/puli/css/style.css' => $file1121,\n            '/webmozart/puli/installer.json' => $file113,\n        );\n\n        $this->assertSame($expected, iterator_to_array($iterator));\n    }\n\n    public function testCurrentAsPath()\n    {\n        $collection = new ArrayResourceCollection(array(\n            new TestDirectory('/webmozart', array(\n                new TestDirectory('/webmozart/puli', array(\n                    new TestDirectory('/webmozart/puli/config', array(\n                        new TestFile('/webmozart/puli/config/config.yml'),\n                        new TestFile('/webmozart/puli/config/routing.yml'),\n                    )),\n                    new TestDirectory('/webmozart/puli/css', array(\n                        new TestFile('/webmozart/puli/css/style.css'),\n                    )),\n                    new TestFile('/webmozart/puli/installer.json'),\n                )),\n            )),\n        ));\n\n        $iterator = new RecursiveResourceIteratorIterator(\n            new ResourceCollectionIterator(\n                $collection,\n                ResourceCollectionIterator::CURRENT_AS_PATH\n            ),\n            RecursiveResourceIteratorIterator::SELF_FIRST\n        );\n\n        $expected = array(\n            '/webmozart' => '/webmozart',\n            '/webmozart/puli' => '/webmozart/puli',\n            '/webmozart/puli/config' => '/webmozart/puli/config',\n            '/webmozart/puli/config/config.yml' => '/webmozart/puli/config/config.yml',\n            '/webmozart/puli/config/routing.yml' => '/webmozart/puli/config/routing.yml',\n            '/webmozart/puli/css' => '/webmozart/puli/css',\n            '/webmozart/puli/css/style.css' => '/webmozart/puli/css/style.css',\n            '/webmozart/puli/installer.json' => '/webmozart/puli/installer.json',\n        );\n\n        $this->assertSame($expected, iterator_to_array($iterator));\n    }\n\n    public function testCurrentAsName()\n    {\n        $collection = new ArrayResourceCollection(array(\n            new TestDirectory('/webmozart', array(\n                new TestDirectory('/webmozart/puli', array(\n                    new TestDirectory('/webmozart/puli/config', array(\n                        new TestFile('/webmozart/puli/config/config.yml'),\n                        new TestFile('/webmozart/puli/config/routing.yml'),\n                    )),\n                    new TestDirectory('/webmozart/puli/css', array(\n                        new TestFile('/webmozart/puli/css/style.css'),\n                    )),\n                    new TestFile('/webmozart/puli/installer.json'),\n                )),\n            )),\n        ));\n\n        $recursiveIterator = new RecursiveResourceIteratorIterator(\n            new ResourceCollectionIterator(\n                $collection,\n                ResourceCollectionIterator::CURRENT_AS_NAME\n            ),\n            RecursiveResourceIteratorIterator::SELF_FIRST\n        );\n\n        $expected = array(\n            '/webmozart' => 'webmozart',\n            '/webmozart/puli' => 'puli',\n            '/webmozart/puli/config' => 'config',\n            '/webmozart/puli/config/config.yml' => 'config.yml',\n            '/webmozart/puli/config/routing.yml' => 'routing.yml',\n            '/webmozart/puli/css' => 'css',\n            '/webmozart/puli/css/style.css' => 'style.css',\n            '/webmozart/puli/installer.json' => 'installer.json',\n        );\n\n        $this->assertSame($expected, iterator_to_array($recursiveIterator));\n    }\n\n    public function testKeyAsPath()\n    {\n        $collection = new ArrayResourceCollection(array(\n            new TestDirectory('/webmozart/puli/config'),\n            new TestDirectory('/webmozart/puli/css'),\n            new TestDirectory('/webmozart/puli/images'),\n            new TestDirectory('/webmozart/puli/installer.json'),\n        ));\n\n        $iterator = new ResourceCollectionIterator(\n            $collection,\n            ResourceCollectionIterator::CURRENT_AS_PATH\n                | ResourceCollectionIterator::KEY_AS_PATH\n        );\n\n        $expected = array(\n            '/webmozart/puli/config' => '/webmozart/puli/config',\n            '/webmozart/puli/css' => '/webmozart/puli/css',\n            '/webmozart/puli/images' => '/webmozart/puli/images',\n            '/webmozart/puli/installer.json' => '/webmozart/puli/installer.json',\n        );\n\n        $this->assertSame($expected, iterator_to_array($iterator));\n    }\n}\n"
  },
  {
    "path": "tests/Resource/Iterator/ResourceFilterIteratorTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests\\Resource\\Iterator;\n\nuse PHPUnit_Framework_TestCase;\nuse Puli\\Repository\\Resource\\Collection\\ArrayResourceCollection;\nuse Puli\\Repository\\Resource\\Iterator\\RecursiveResourceIteratorIterator;\nuse Puli\\Repository\\Resource\\Iterator\\ResourceCollectionIterator;\nuse Puli\\Repository\\Resource\\Iterator\\ResourceFilterIterator;\nuse Puli\\Repository\\Tests\\Resource\\TestDirectory;\nuse Puli\\Repository\\Tests\\Resource\\TestFile;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass ResourceFilterIteratorTest extends PHPUnit_Framework_TestCase\n{\n    /**\n     * @var ArrayResourceCollection\n     */\n    private $collection;\n\n    protected function setUp()\n    {\n        $this->collection = new ArrayResourceCollection(array(\n            new TestDirectory('/webmozart', array(\n                new TestDirectory('/webmozart/puli', array(\n                    new TestDirectory('/webmozart/puli/config', array(\n                        new TestFile('/webmozart/puli/config/config.yml'),\n                        new TestFile('/webmozart/puli/config/routing.yml'),\n                    )),\n                    new TestDirectory('/webmozart/puli/css', array(\n                        new TestDirectory('/webmozart/puli/css/bootstrap', array(\n                            new TestFile('/webmozart/puli/css/bootstrap/bootstrap.css'),\n                        )),\n                        new TestFile('/webmozart/puli/css/fonts.css'),\n                        new TestFile('/webmozart/puli/css/style.css'),\n                    )),\n                    new TestFile('/webmozart/puli/installer.json'),\n                )),\n            )),\n        ));\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testRejectEmptyPattern()\n    {\n        $innerIterator = new ResourceCollectionIterator(new ArrayResourceCollection());\n\n        new ResourceFilterIterator($innerIterator, '');\n    }\n\n    public function testFilterPathPrefix()\n    {\n        $iterator = new ResourceFilterIterator(\n            new RecursiveResourceIteratorIterator(\n                new ResourceCollectionIterator(\n                    $this->collection,\n                    ResourceCollectionIterator::KEY_AS_PATH | ResourceCollectionIterator::CURRENT_AS_NAME\n                ),\n                RecursiveResourceIteratorIterator::SELF_FIRST\n            ),\n            '/webmozart/puli/css',\n            ResourceFilterIterator::MATCH_PREFIX\n        );\n\n        $expected = array(\n            '/webmozart/puli/css' => 'css',\n            '/webmozart/puli/css/bootstrap' => 'bootstrap',\n            '/webmozart/puli/css/bootstrap/bootstrap.css' => 'bootstrap.css',\n            '/webmozart/puli/css/fonts.css' => 'fonts.css',\n            '/webmozart/puli/css/style.css' => 'style.css',\n        );\n\n        $this->assertSame($expected, iterator_to_array($iterator));\n    }\n\n    public function testFilterPathSuffix()\n    {\n        $iterator = new ResourceFilterIterator(\n            new RecursiveResourceIteratorIterator(\n                new ResourceCollectionIterator(\n                    $this->collection,\n                    ResourceCollectionIterator::KEY_AS_PATH | ResourceCollectionIterator::CURRENT_AS_NAME\n                ),\n                RecursiveResourceIteratorIterator::SELF_FIRST\n            ),\n            '.css',\n            ResourceFilterIterator::MATCH_SUFFIX\n        );\n\n        $expected = array(\n            '/webmozart/puli/css/bootstrap/bootstrap.css' => 'bootstrap.css',\n            '/webmozart/puli/css/fonts.css' => 'fonts.css',\n            '/webmozart/puli/css/style.css' => 'style.css',\n        );\n\n        $this->assertSame($expected, iterator_to_array($iterator));\n    }\n\n    public function testFilterPathRegexImplicit()\n    {\n        $iterator = new ResourceFilterIterator(\n            new RecursiveResourceIteratorIterator(\n                new ResourceCollectionIterator(\n                    $this->collection,\n                    ResourceCollectionIterator::KEY_AS_PATH | ResourceCollectionIterator::CURRENT_AS_NAME\n                ),\n                RecursiveResourceIteratorIterator::SELF_FIRST\n            ),\n            '/\\.css$/'\n        );\n\n        $expected = array(\n            '/webmozart/puli/css/bootstrap/bootstrap.css' => 'bootstrap.css',\n            '/webmozart/puli/css/fonts.css' => 'fonts.css',\n            '/webmozart/puli/css/style.css' => 'style.css',\n        );\n\n        $this->assertSame($expected, iterator_to_array($iterator));\n    }\n\n    public function testFilterPathRegexExplicit()\n    {\n        $iterator = new ResourceFilterIterator(\n            new RecursiveResourceIteratorIterator(\n                new ResourceCollectionIterator(\n                    $this->collection,\n                    ResourceCollectionIterator::KEY_AS_PATH | ResourceCollectionIterator::CURRENT_AS_NAME\n                ),\n                RecursiveResourceIteratorIterator::SELF_FIRST\n            ),\n            '/\\.css$/',\n            ResourceFilterIterator::MATCH_REGEX\n        );\n\n        $expected = array(\n            '/webmozart/puli/css/bootstrap/bootstrap.css' => 'bootstrap.css',\n            '/webmozart/puli/css/fonts.css' => 'fonts.css',\n            '/webmozart/puli/css/style.css' => 'style.css',\n        );\n\n        $this->assertSame($expected, iterator_to_array($iterator));\n    }\n\n    public function testFilterNamePrefix()\n    {\n        $iterator = new ResourceFilterIterator(\n            new RecursiveResourceIteratorIterator(\n                new ResourceCollectionIterator(\n                    $this->collection,\n                    ResourceCollectionIterator::KEY_AS_PATH | ResourceCollectionIterator::CURRENT_AS_NAME\n                ),\n                RecursiveResourceIteratorIterator::SELF_FIRST\n            ),\n            'bootstrap',\n            ResourceFilterIterator::FILTER_BY_NAME | ResourceFilterIterator::MATCH_PREFIX\n        );\n\n        $expected = array(\n            '/webmozart/puli/css/bootstrap' => 'bootstrap',\n            '/webmozart/puli/css/bootstrap/bootstrap.css' => 'bootstrap.css',\n        );\n\n        $this->assertSame($expected, iterator_to_array($iterator));\n    }\n}\n"
  },
  {
    "path": "tests/Resource/LinkResourceTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests\\Resource;\n\nuse Puli\\Repository\\Resource\\Collection\\ArrayResourceCollection;\nuse Puli\\Repository\\Resource\\LinkResource;\n\n/**\n * @author Titouan Galopin <galopintitouan@gmail.com>\n */\nclass LinkResourceTest extends AbstractResourceTest\n{\n    protected function createResource($path = null)\n    {\n        return new LinkResource('/target-path', $path);\n    }\n\n    public function testListChildren()\n    {\n        $file1 = new TestFile('/file1');\n        $file2 = new TestFile('/file2');\n        $resources = new ArrayResourceCollection(array($file1, $file2));\n        $repo = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n\n        $repo->expects($this->once())\n            ->method('listChildren')\n            ->with('/target-path')\n            ->will($this->returnValue($resources));\n\n        $resource = $this->createResource('/path');\n        $resource->attachTo($repo);\n\n        $children = $resource->listChildren();\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\ResourceCollection', $children);\n        $this->assertEquals(array('file1' => $file1, 'file2' => $file2), $children->toArray());\n    }\n\n    public function testListChildrenWithReference()\n    {\n        $file1 = new TestFile('/file1');\n        $file2 = new TestFile('/file2');\n        $resources = new ArrayResourceCollection(array($file1, $file2));\n        $repo = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n\n        $repo->expects($this->once())\n            ->method('listChildren')\n            // use the repository path, not the reference path\n            ->with('/target-path')\n            ->will($this->returnValue($resources));\n\n        $resource = $this->createResource('/path');\n        $resource->attachTo($repo);\n\n        $reference = $resource->createReference('/reference');\n\n        $children = $reference->listChildren();\n\n        $this->assertInstanceOf('Puli\\Repository\\Api\\ResourceCollection', $children);\n        $this->assertEquals(array('file1' => $file1, 'file2' => $file2), $children->toArray());\n    }\n\n    public function testGetChild()\n    {\n        $child = $this->getMock('Puli\\Repository\\Api\\Resource\\PuliResource');\n        $repo = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n\n        $repo->expects($this->once())\n            ->method('get')\n            ->with('/target-path/file')\n            ->will($this->returnValue($child));\n\n        $resource = $this->createResource('/path');\n        $resource->attachTo($repo);\n\n        $this->assertSame($child, $resource->getChild('file'));\n    }\n\n    public function testGetChildWithReference()\n    {\n        $child = $this->getMock('Puli\\Repository\\Api\\Resource\\PuliResource');\n        $repo = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n\n        $repo->expects($this->once())\n            ->method('get')\n            // use the repository path, not the reference path\n            ->with('/target-path/file')\n            ->will($this->returnValue($child));\n\n        $resource = $this->createResource('/path');\n        $resource->attachTo($repo);\n\n        $reference = $resource->createReference('/reference');\n\n        $this->assertSame($child, $reference->getChild('file'));\n    }\n\n    public function testHasChild()\n    {\n        $repo = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n\n        $repo->expects($this->once())\n            ->method('contains')\n            ->with('/target-path/file')\n            ->will($this->returnValue('true_or_false'));\n\n        $resource = $this->createResource('/path');\n        $resource->attachTo($repo);\n\n        $this->assertSame('true_or_false', $resource->hasChild('file'));\n    }\n\n    public function testHasChildWithReference()\n    {\n        $repo = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n\n        $repo->expects($this->once())\n            ->method('contains')\n            // use the repository path, not the reference path\n            ->with('/target-path/file')\n            ->will($this->returnValue('true_or_false'));\n\n        $resource = $this->createResource('/path');\n        $resource->attachTo($repo);\n\n        $reference = $resource->createReference('/reference');\n\n        $this->assertSame('true_or_false', $reference->hasChild('file'));\n    }\n\n    public function testSerializeKeepsTargetPath()\n    {\n        $resource = $this->createResource('/link-path');\n\n        $deserialized = unserialize(serialize($resource));\n\n        $this->assertEquals($resource->getTargetPath(), $deserialized->getTargetPath());\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\ResourceNotFoundException\n     */\n    public function testGetTargetFailsIfNoRepository()\n    {\n        $resource = $this->createResource('/path');\n        $resource->getTarget();\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\ResourceNotFoundException\n     */\n    public function testGetChildFailsIfNoRepository()\n    {\n        $resource = $this->createResource('/path');\n        $resource->getChild('/child');\n    }\n}\n"
  },
  {
    "path": "tests/Resource/TestDirectory.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests\\Resource;\n\nuse Puli\\Repository\\Api\\Resource\\PuliResource;\nuse Puli\\Repository\\Resource\\Collection\\ArrayResourceCollection;\nuse Puli\\Repository\\Resource\\GenericResource;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass TestDirectory extends GenericResource\n{\n    /**\n     * @var PuliResource[]\n     */\n    private $children = array();\n\n    private $metadata;\n\n    public function __construct($path = null, array $children = array())\n    {\n        parent::__construct($path);\n\n        foreach ($children as $child) {\n            $this->children[$child->getName()] = $child;\n        }\n\n        $this->metadata = new TestMetadata();\n    }\n\n    public function getChild($relPath)\n    {\n        return $this->children[$relPath];\n    }\n\n    public function hasChild($relPath)\n    {\n        return isset($this->children[$relPath]);\n    }\n\n    public function hasChildren()\n    {\n        return count($this->children) > 0;\n    }\n\n    public function listChildren()\n    {\n        return new ArrayResourceCollection($this->children);\n    }\n\n    public function getMetadata()\n    {\n        return $this->metadata;\n    }\n}\n"
  },
  {
    "path": "tests/Resource/TestFile.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests\\Resource;\n\nuse Puli\\Repository\\Api\\Resource\\BodyResource;\nuse Puli\\Repository\\Api\\ResourceNotFoundException;\nuse Puli\\Repository\\Resource\\Collection\\ArrayResourceCollection;\nuse Puli\\Repository\\Resource\\GenericResource;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass TestFile extends GenericResource implements BodyResource\n{\n    const BODY = \"LINE 1\\nLINE 2\\n\";\n\n    private $body;\n\n    private $metadata;\n\n    public function __construct($path = null, $body = self::BODY)\n    {\n        parent::__construct($path);\n\n        $this->body = $body;\n        $this->metadata = new TestMetadata();\n    }\n\n    public function getBody()\n    {\n        return $this->body;\n    }\n\n    public function getSize()\n    {\n        return strlen($this->body);\n    }\n\n    public function getChild($relPath)\n    {\n        throw ResourceNotFoundException::forPath($this->getPath().'/'.$relPath);\n    }\n\n    public function hasChild($relPath)\n    {\n        return false;\n    }\n\n    public function hasChildren()\n    {\n        return false;\n    }\n\n    public function listChildren()\n    {\n        return new ArrayResourceCollection();\n    }\n\n    public function getMetadata()\n    {\n        return $this->metadata;\n    }\n}\n"
  },
  {
    "path": "tests/Resource/TestMetadata.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests\\Resource;\n\nuse Puli\\Repository\\Api\\Resource\\ResourceMetadata;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass TestMetadata extends ResourceMetadata\n{\n    /**\n     * @var int\n     */\n    private $creationTime = 0;\n\n    /**\n     * @var int\n     */\n    private $accessTime = 0;\n\n    /**\n     * @var int\n     */\n    private $modificationTime = 0;\n\n    /**\n     * @var int\n     */\n    private $size = 0;\n\n    /**\n     * @return int\n     */\n    public function getCreationTime()\n    {\n        return $this->creationTime;\n    }\n\n    /**\n     * @param int $creationTime\n     */\n    public function setCreationTime($creationTime)\n    {\n        $this->creationTime = $creationTime;\n    }\n\n    /**\n     * @return int\n     */\n    public function getAccessTime()\n    {\n        return $this->accessTime;\n    }\n\n    /**\n     * @param int $accessTime\n     */\n    public function setAccessTime($accessTime)\n    {\n        $this->accessTime = $accessTime;\n    }\n\n    /**\n     * @return int\n     */\n    public function getModificationTime()\n    {\n        return $this->modificationTime;\n    }\n\n    /**\n     * @param int $modificationTime\n     */\n    public function setModificationTime($modificationTime)\n    {\n        $this->modificationTime = $modificationTime;\n    }\n\n    /**\n     * @return int\n     */\n    public function getSize()\n    {\n        return $this->size;\n    }\n\n    /**\n     * @param int $size\n     */\n    public function setSize($size)\n    {\n        $this->size = $size;\n    }\n}\n"
  },
  {
    "path": "tests/StreamWrapper/ResourceStreamWrapperTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests\\StreamWrapper;\n\nuse PHPUnit_Framework_Assert;\nuse PHPUnit_Framework_TestCase;\nuse Puli\\Repository\\Api\\ResourceNotFoundException;\nuse Puli\\Repository\\Api\\ResourceRepository;\nuse Puli\\Repository\\Resource\\FileResource;\nuse Puli\\Repository\\StreamWrapper\\ResourceStreamWrapper;\nuse Puli\\Repository\\Tests\\Resource\\TestDirectory;\nuse Puli\\Repository\\Tests\\Resource\\TestFile;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass ResourceStreamWrapperTest extends PHPUnit_Framework_TestCase\n{\n    const FILE_CONTENTS = \"LINE 1\\nLINE 2\\n\";\n\n    /**\n     * @var resource\n     */\n    private $handle;\n\n    /**\n     * @var resource\n     */\n    private $dir;\n\n    /**\n     * @var resource\n     */\n    private $dir2;\n\n    /**\n     * @var ResourceRepository\n     */\n    private $repo;\n\n    protected function setUp()\n    {\n        $tempnam = tempnam(sys_get_temp_dir(), 'ResourceStreamWrapperTest');\n        file_put_contents($tempnam, self::FILE_CONTENTS);\n\n        $this->repo = $this->getMock('Puli\\Repository\\Api\\ResourceRepository');\n\n        $this->repo->expects($this->any())\n            ->method('get')\n            ->will($this->returnCallback(function ($path) use ($tempnam) {\n                if ('/webmozart/puli/file' === $path) {\n                    return new FileResource($tempnam, '/webmozart/puli/file');\n                }\n                if ('/webmozart/puli/non-local' === $path) {\n                    return new TestFile('/webmozart/puli/non-local');\n                }\n                if ('/webmozart/puli/dir' === $path) {\n                    return new TestDirectory('/webmozart/puli/dir', array(\n                        new TestFile('/webmozart/puli/dir/file1'),\n                        new TestFile('/webmozart/puli/dir/file2'),\n                    ));\n                }\n                if ('/webmozart/puli/dir2' === $path) {\n                    return new TestDirectory('/webmozart/puli/dir2', array(\n                        new TestFile('/webmozart/puli/dir2/.dotfile'),\n                        new TestFile('/webmozart/puli/dir2/foo'),\n                        new TestFile('/webmozart/puli/dir2/bar'),\n                    ));\n                }\n\n                throw new ResourceNotFoundException();\n            }));\n    }\n\n    protected function tearDown()\n    {\n        ResourceStreamWrapper::unregister('puli');\n\n        if (is_resource($this->handle)) {\n            fclose($this->handle);\n        }\n\n        if (is_resource($this->dir)) {\n            closedir($this->dir);\n        }\n\n        if (is_resource($this->dir2)) {\n            closedir($this->dir2);\n        }\n\n        if (in_array('manual', stream_get_wrappers())) {\n            stream_wrapper_unregister('manual');\n        }\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedResourceException\n     */\n    public function testOpenNonFile()\n    {\n        ResourceStreamWrapper::register('puli', $this->repo);\n\n        fopen('puli:///webmozart/puli/dir', 'r');\n    }\n\n    public function provideFilePaths()\n    {\n        return array(\n            array('puli:///webmozart/puli/file'),\n            array('puli:///webmozart/puli/non-local'),\n        );\n    }\n\n    /**\n     * @dataProvider provideFilePaths\n     */\n    public function testRead($path)\n    {\n        ResourceStreamWrapper::register('puli', $this->repo);\n\n        $this->handle = fopen($path, 'r');\n\n        $this->assertInternalType('resource', $this->handle);\n        $this->assertSame('LI', fread($this->handle, 2));\n        $this->assertSame(\"NE 1\\n\", fgets($this->handle));\n        $this->assertSame(\"LINE 2\\n\", fgets($this->handle));\n        $this->assertFalse(fgets($this->handle));\n        $this->assertTrue(feof($this->handle));\n        $this->assertTrue(fclose($this->handle));\n    }\n\n    /**\n     * @dataProvider provideFilePaths\n     */\n    public function testSeekSet($path)\n    {\n        ResourceStreamWrapper::register('puli', $this->repo);\n\n        $this->handle = fopen($path, 'r');\n\n        fread($this->handle, 2);\n\n        $this->assertSame(0, fseek($this->handle, 1, SEEK_SET));\n        $this->assertSame('INE 1', fread($this->handle, 5));\n        $this->assertSame(6, ftell($this->handle));\n    }\n\n    /**\n     * @dataProvider provideFilePaths\n     */\n    public function testSeekCur($path)\n    {\n        ResourceStreamWrapper::register('puli', $this->repo);\n\n        $this->handle = fopen($path, 'r');\n\n        fread($this->handle, 2);\n\n        $this->assertSame(0, fseek($this->handle, 1, SEEK_CUR));\n        $this->assertSame('E 1', fread($this->handle, 3));\n        $this->assertSame(6, ftell($this->handle));\n    }\n\n    public function testSeekInvalidPositiveOffsetLocal()\n    {\n        ResourceStreamWrapper::register('puli', $this->repo);\n\n        $this->handle = fopen('puli:///webmozart/puli/file', 'r');\n\n        // fseek() lets you seek non-existing positive offsets\n        $this->assertSame(0, fseek($this->handle, 1000000));\n\n        // the cursor is actually changed\n        $this->assertSame(1000000, ftell($this->handle));\n    }\n\n    public function testSeekInvalidPositiveOffsetNonLocal()\n    {\n        ResourceStreamWrapper::register('puli', $this->repo);\n\n        $this->handle = fopen('puli:///webmozart/puli/non-local', 'r');\n\n        fseek($this->handle, 5);\n\n        // php://temp does NOT let you seek non-existing positive offsets\n        $this->assertSame(-1, fseek($this->handle, 1000000));\n\n        // the cursor is not changed\n        $this->assertSame(5, ftell($this->handle));\n    }\n\n    /**\n     * @dataProvider provideFilePaths\n     */\n    public function testSeekInvalidNegativeOffset($path)\n    {\n        ResourceStreamWrapper::register('puli', $this->repo);\n\n        $this->handle = fopen($path, 'r');\n\n        fseek($this->handle, 5);\n\n        // large negative offsets are not allowed\n        $this->assertSame(-1, fseek($this->handle, -1000000));\n\n        // the cursor is not changed\n        $this->assertSame(5, ftell($this->handle));\n    }\n\n    public function testStatLocal()\n    {\n        ResourceStreamWrapper::register('puli', $this->repo);\n\n        $this->handle = fopen('puli:///webmozart/puli/file', 'r');\n\n        $stat = fstat($this->handle);\n\n        $this->assertInternalType('array', $stat);\n        $this->assertArrayHasKey('dev', $stat);\n        $this->assertArrayHasKey('ino', $stat);\n        $this->assertArrayHasKey('mode', $stat);\n    }\n\n    public function testStatNonLocal()\n    {\n        ResourceStreamWrapper::register('puli', $this->repo);\n\n        $this->handle = fopen('puli:///webmozart/puli/non-local', 'r');\n\n        $stat = fstat($this->handle);\n\n        $this->assertInternalType('array', $stat);\n        $this->assertArrayHasKey('dev', $stat);\n        $this->assertArrayHasKey('ino', $stat);\n        $this->assertArrayHasKey('mode', $stat);\n    }\n\n    public function testIsLocal()\n    {\n        ResourceStreamWrapper::register('puli', $this->repo);\n\n        $this->handle = fopen('puli:///webmozart/puli/file', 'r');\n\n        $this->assertTrue(stream_is_local($this->handle));\n    }\n\n    public function testIsNonLocal()\n    {\n        ResourceStreamWrapper::register('puli', $this->repo);\n\n        $this->handle = fopen('puli:///webmozart/puli/non-local', 'r');\n\n        // stream_is_local() only returns false if STREAM_IS_URL is passed\n        // when registering the \"puli\" protocol\n        $this->assertTrue(stream_is_local($this->handle));\n    }\n\n    public function testExists()\n    {\n        ResourceStreamWrapper::register('puli', $this->repo);\n\n        // uses url_stat()\n        $this->assertTrue(file_exists('puli:///webmozart/puli/file'));\n        $this->assertTrue(file_exists('puli:///webmozart/puli/dir'));\n        $this->assertTrue(file_exists('puli:///webmozart/puli/non-local'));\n        $this->assertFalse(file_exists('puli:///webmozart/puli/foobar'));\n    }\n\n    /**\n     * @dataProvider provideFilePaths\n     */\n    public function testSelect($path)\n    {\n        ResourceStreamWrapper::register('puli', $this->repo);\n\n        $this->handle = fopen($path, 'r');\n\n        $read = array($this->handle);\n        $write = array();\n        $exceptional = array();\n        $timeoutSec = 0;\n        $timeoutMs = 0;\n\n        $this->assertSame(\n            1,\n            stream_select($read, $write, $exceptional, $timeoutSec, $timeoutMs)\n        );\n    }\n\n    /**\n     * @dataProvider provideWriteModes\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedOperationException\n     */\n    public function testCannotOpenForWriting($mode)\n    {\n        ResourceStreamWrapper::register('puli', $this->repo);\n\n        fopen('puli:///webmozart/puli/file', $mode);\n    }\n\n    public function provideWriteModes()\n    {\n        return array(\n            array('r+'),\n            array('w'),\n            array('w+'),\n            array('a'),\n            array('a+'),\n            array('x'),\n            array('x+'),\n            array('c'),\n            array('c+'),\n        );\n    }\n\n    /**\n     * @dataProvider provideFilePaths\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedOperationException\n     */\n    public function testLockIsProhibited($path)\n    {\n        ResourceStreamWrapper::register('puli', $this->repo);\n\n        $this->handle = fopen($path, 'r');\n        flock($this->handle, LOCK_SH);\n    }\n\n    /**\n     * @dataProvider provideFilePaths\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedOperationException\n     */\n    public function testTouchExistingIsProhibited($path)\n    {\n        if (version_compare(PHP_VERSION, '5.4.0', '<')) {\n            $this->markTestSkipped('Only supported in PHP 5.4+.');\n\n            return;\n        }\n\n        ResourceStreamWrapper::register('puli', $this->repo);\n\n        touch($path);\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedOperationException\n     */\n    public function testTouchNewIsProhibited()\n    {\n        if (version_compare(PHP_VERSION, '5.4.0', '<')) {\n            $this->markTestSkipped('Only supported in PHP 5.4+.');\n\n            return;\n        }\n\n        ResourceStreamWrapper::register('puli', $this->repo);\n\n        touch('puli:///webmozart/puli/new');\n    }\n\n    /**\n     * @dataProvider provideFilePaths\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedOperationException\n     */\n    public function testChownIsProhibited($path)\n    {\n        if (version_compare(PHP_VERSION, '5.4.0', '<')) {\n            $this->markTestSkipped('Only supported in PHP 5.4+.');\n\n            return;\n        }\n\n        ResourceStreamWrapper::register('puli', $this->repo);\n\n        chown($path, 'root');\n    }\n\n    /**\n     * @dataProvider provideFilePaths\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedOperationException\n     */\n    public function testChgrpIsProhibited($path)\n    {\n        if (version_compare(PHP_VERSION, '5.4.0', '<')) {\n            $this->markTestSkipped('Only supported in PHP 5.4+.');\n\n            return;\n        }\n\n        ResourceStreamWrapper::register('puli', $this->repo);\n\n        chgrp($path, 'root');\n    }\n\n    /**\n     * @dataProvider provideFilePaths\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedOperationException\n     */\n    public function testChmodIsProhibited($path)\n    {\n        if (version_compare(PHP_VERSION, '5.4.0', '<')) {\n            $this->markTestSkipped('Only supported in PHP 5.4+.');\n\n            return;\n        }\n\n        ResourceStreamWrapper::register('puli', $this->repo);\n\n        chmod($path, 0777);\n    }\n\n    /**\n     * @dataProvider provideFilePaths\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedOperationException\n     */\n    public function testUnlinkIsProhibited($path)\n    {\n        ResourceStreamWrapper::register('puli', $this->repo);\n\n        unlink($path);\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedOperationException\n     */\n    public function testRenameIsProhibited()\n    {\n        ResourceStreamWrapper::register('puli', $this->repo);\n\n        rename('puli:///webmozart/puli/file', 'puli:///webmozart/puli/baz');\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedOperationException\n     */\n    public function testRmdirIsProhibited()\n    {\n        ResourceStreamWrapper::register('puli', $this->repo);\n\n        rmdir('puli:///webmozart/puli/dir');\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\UnsupportedOperationException\n     */\n    public function testMkdirIsProhibited()\n    {\n        ResourceStreamWrapper::register('puli', $this->repo);\n\n        mkdir('puli:///webmozart/puli/new-dir');\n    }\n\n    public function testListDirectory()\n    {\n        ResourceStreamWrapper::register('puli', $this->repo);\n\n        $this->dir = opendir('puli:///webmozart/puli/dir');\n\n        $this->assertInternalType('resource', $this->dir);\n        $this->assertSame('file1', readdir($this->dir));\n        $this->assertSame('file2', readdir($this->dir));\n        $this->assertFalse(readdir($this->dir));\n\n        rewinddir($this->dir);\n\n        $this->assertSame('file1', readdir($this->dir));\n    }\n\n    public function testListMultipleDirectories()\n    {\n        ResourceStreamWrapper::register('puli', $this->repo);\n\n        // Test whether simultaneous traversal works\n        $this->dir = opendir('puli:///webmozart/puli/dir');\n        $this->dir2 = opendir('puli:///webmozart/puli/dir2');\n\n        $this->assertSame('file1', readdir($this->dir));\n        $this->assertSame('.dotfile', readdir($this->dir2));\n        $this->assertSame('file2', readdir($this->dir));\n        $this->assertSame('foo', readdir($this->dir2));\n        $this->assertFalse(readdir($this->dir));\n        $this->assertSame('bar', readdir($this->dir2));\n        $this->assertFalse(readdir($this->dir2));\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\Api\\ResourceNotFoundException\n     */\n    public function testOpenNonExistingDirectory()\n    {\n        ResourceStreamWrapper::register('puli', $this->repo);\n\n        opendir('puli:///webmozart/puli/foobar');\n    }\n\n    public function testRegisterCallable()\n    {\n        $repo = $this->repo;\n\n        ResourceStreamWrapper::register('puli', function () use ($repo) {\n            return $repo;\n        });\n\n        $this->assertSame(self::FILE_CONTENTS, file_get_contents('puli:///webmozart/puli/file'));\n    }\n\n    public function testCallableNotInvokedIfNotUsed()\n    {\n        $repo = $this->repo;\n\n        ResourceStreamWrapper::register('puli', function () use ($repo) {\n            return $repo;\n        });\n\n        ResourceStreamWrapper::register('unused', function () {\n            PHPUnit_Framework_Assert::fail('Should not be called.');\n        });\n\n        $this->assertSame(self::FILE_CONTENTS, file_get_contents('puli:///webmozart/puli/file'));\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\RepositoryFactoryException\n     * @expectedExceptionMessage stdClass\n     */\n    public function testFailIfCallableDoesNotReturnValidRepository()\n    {\n        ResourceStreamWrapper::register('puli', function () {\n            return new \\stdClass();\n        });\n\n        file_get_contents('puli:///webmozart/puli/file');\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\StreamWrapper\\StreamWrapperException\n     */\n    public function testRegisterTwice()\n    {\n        ResourceStreamWrapper::register('puli', $this->repo);\n        ResourceStreamWrapper::register('puli', $this->repo);\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     */\n    public function testRegisterFailsIfNotRepoNorCallable()\n    {\n        ResourceStreamWrapper::register('puli', 'foobar');\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     * @expectedExceptionMessage Got: integer\n     */\n    public function testRegisterFailsIfSchemeNotString()\n    {\n        ResourceStreamWrapper::register(1234, $this->repo);\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     * @expectedExceptionMessage letters and digits\n     */\n    public function testRegisterFailsIfSchemeContainsSpecialChars()\n    {\n        ResourceStreamWrapper::register('puli{', $this->repo);\n    }\n\n    /**\n     * @expectedException \\InvalidArgumentException\n     * @expectedExceptionMessage start with a letter\n     */\n    public function testRegisterFailsIfSchemeDoesNotStartWithLetter()\n    {\n        ResourceStreamWrapper::register('1puli', $this->repo);\n    }\n\n    public function testUnregisterIsIdempotent()\n    {\n        ResourceStreamWrapper::unregister('puli');\n        ResourceStreamWrapper::unregister('puli');\n    }\n\n    /**\n     * @expectedException \\Puli\\Repository\\StreamWrapper\\StreamWrapperException\n     */\n    public function testWrapperShouldNotBeRegisteredManually()\n    {\n        stream_wrapper_register('manual', '\\Puli\\Repository\\StreamWrapper\\ResourceStreamWrapper');\n\n        fopen('manual:///webmozart/puli/file1', 'r');\n    }\n}\n"
  },
  {
    "path": "tests/Uri/UriTest.php",
    "content": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\nnamespace Puli\\Repository\\Tests\\Uri;\n\nuse PHPUnit_Framework_TestCase;\nuse Puli\\Repository\\Uri\\Uri;\n\n/**\n * @author Bernhard Schussek <bschussek@gmail.com>\n */\nclass UriTest extends PHPUnit_Framework_TestCase\n{\n    public function provideValidUris()\n    {\n        return array(\n            array('scheme:///path/to/resource', array(\n                'scheme' => 'scheme',\n                'path' => '/path/to/resource',\n            )),\n            array('psr4:///path/to/resource', array(\n                'scheme' => 'psr4',\n                'path' => '/path/to/resource',\n            )),\n            array('/path/to/resource', array(\n                'scheme' => '',\n                'path' => '/path/to/resource',\n            )),\n        );\n    }\n\n    /**\n     * @dataProvider provideValidUris\n     */\n    public function testParse($uri, $parts)\n    {\n        $this->assertEquals($parts, Uri::parse($uri));\n    }\n\n    public function provideInvalidUris()\n    {\n        return array(\n            array(''),\n            array(null),\n            array(123),\n            array(new \\stdClass()),\n            array(':///path/to/resource'),\n            array('1foo:///path/to/resource'),\n            array('foo@:///path/to/resource'),\n            array('scheme:/path/to/resource'),\n            array('scheme//path/to/resource'),\n            array('scheme:://path/to/resource'),\n        );\n    }\n\n    /**\n     * @dataProvider provideInvalidUris\n     * @expectedException \\Puli\\Repository\\Uri\\InvalidUriException\n     */\n    public function testParseInvalid($uri)\n    {\n        Uri::parse($uri);\n    }\n}\n"
  }
]